com.gelakinetic.mtgfam.fragments.CardViewFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.gelakinetic.mtgfam.fragments.CardViewFragment.java

Source

/**
 * Copyright 2011 Adam Feinstein
 * <p/>
 * This file is part of MTG Familiar.
 * <p/>
 * MTG Familiar 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.
 * <p/>
 * MTG Familiar 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.
 * <p/>
 * You should have received a copy of the GNU General Public License
 * along with MTG Familiar.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.gelakinetic.mtgfam.fragments;

import android.Manifest;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.FileProvider;
import android.text.Html;
import android.text.Html.ImageGetter;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.util.TypedValue;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import com.gelakinetic.GathererScraper.JsonTypes.Card;
import com.gelakinetic.GathererScraper.Language;
import com.gelakinetic.mtgfam.FamiliarActivity;
import com.gelakinetic.mtgfam.R;
import com.gelakinetic.mtgfam.fragments.dialogs.CardViewDialogFragment;
import com.gelakinetic.mtgfam.fragments.dialogs.FamiliarDialogFragment;
import com.gelakinetic.mtgfam.helpers.AppIndexingWrapper;
import com.gelakinetic.mtgfam.helpers.ColorIndicatorView;
import com.gelakinetic.mtgfam.helpers.ImageGetterHelper;
import com.gelakinetic.mtgfam.helpers.PriceFetchRequest;
import com.gelakinetic.mtgfam.helpers.PriceInfo;
import com.gelakinetic.mtgfam.helpers.SearchCriteria;
import com.gelakinetic.mtgfam.helpers.ToastWrapper;
import com.gelakinetic.mtgfam.helpers.database.CardDbAdapter;
import com.gelakinetic.mtgfam.helpers.database.DatabaseManager;
import com.gelakinetic.mtgfam.helpers.database.FamiliarDbException;
import com.gelakinetic.mtgfam.helpers.lruCache.RecyclingBitmapDrawable;
import com.octo.android.robospice.persistence.DurationInMillis;
import com.octo.android.robospice.persistence.exception.SpiceException;
import com.octo.android.robospice.request.listener.RequestListener;

import org.apache.commons.io.IOUtils;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Locale;

/**
 * This class handles displaying card info
 * WARNING! Because this fragment is nested in a CardViewPagerFragment, always get the parent fragment's activity
 */
public class CardViewFragment extends FamiliarFragment {

    /* Bundle constant */
    public static final String CARD_ID = "card_id";

    /* Where the card image is loaded to */
    public static final int MAIN_PAGE = 1;
    private static final int DIALOG = 2;
    private static final int SHARE = 3;
    /* Used to store the String when copying to clipboard */
    private String mCopyString;
    /* UI elements, to be filled in */
    private TextView mNameTextView;
    private TextView mCostTextView;
    private TextView mTypeTextView;
    private TextView mSetTextView;
    private TextView mAbilityTextView;
    private TextView mPowTouTextView;
    private TextView mFlavorTextView;
    private TextView mArtistTextView;
    private TextView mNumberTextView;
    private Button mTransformButton;
    private View mTransformButtonDivider;
    private ImageView mCardImageView;
    private ScrollView mTextScrollView;
    private ScrollView mImageScrollView;
    private LinearLayout mColorIndicatorLayout;

    /* the AsyncTask loads stuff off the UI thread, and stores whatever in these local variables */
    public AsyncTask mAsyncTask;
    public RecyclingBitmapDrawable mCardBitmap;
    public String[] mLegalities;
    public String[] mFormats;
    public ArrayList<Ruling> mRulingsArrayList;

    /* Loaded in a Spice Service */
    public PriceInfo mPriceInfo;

    /* Card info, used to build the URL to fetch the picture */
    private String mCardNumber;
    private String mSetCode;
    public String mCardName;
    private int mCardCMC;
    private String mMagicCardsInfoSetCode;
    public int mMultiverseId;
    private String mCardType;

    /* Card info used to flip the card */
    private String mTransformCardNumber;
    private int mTransformId;

    /* To switch card between printings */
    public LinkedHashSet<String> mPrintings;
    public LinkedHashSet<Long> mCardIds;

    /* Easier than calling getActivity() all the time, and handles being nested */
    private FamiliarActivity mActivity;

    /* State for reporting page views */
    private boolean mHasReportedView = false;
    private boolean mShouldReportView = false;
    public String mDescription;
    public String mSetName;

    /* Foreign name translations */
    public ArrayList<Card.ForeignPrinting> mTranslatedNames = new ArrayList<>();

    /**
     * Kill any AsyncTask if it is still running
     */
    @Override
    public void onDestroy() {
        super.onDestroy();

        /* Pass a non-null bundle to the ResultListFragment so it knows to exit if there was a list of 1 card
         * If this wasn't launched by a ResultListFragment, it'll get eaten */
        Bundle args = new Bundle();
        if (mActivity != null) {
            mActivity.setFragmentResult(args);
        }
    }

    /**
     * Called when the Fragment is no longer resumed. Clear the loading bar just in case
     */
    @Override
    public void onPause() {
        super.onPause();
        if (mActivity != null) {
            mActivity.clearLoading();
        }
        if (mAsyncTask != null) {
            mAsyncTask.cancel(true);
        }
    }

    /**
     * Called when the fragment stops, attempt to report the close
     */
    @Override
    public void onStop() {
        reportAppIndexEndIfAble();
        super.onStop();
    }

    /**
     * Reports this view to the Google app indexing API, once, when the fragment is viewed
     */
    private void reportAppIndexViewIfAble() {
        /* If this view hasn't been reported yet, and the name exists */
        if (!mHasReportedView) {
            if (mCardName != null) {
                /* Connect your client */
                getFamiliarActivity().mAppIndexingWrapper.connect();
                AppIndexingWrapper.startAppIndexing(getFamiliarActivity().mAppIndexingWrapper, this);

                /* Manage state */
                mHasReportedView = true;
                mShouldReportView = false;
            } else {
                mShouldReportView = true;
            }
        }
    }

    /**
     * Ends the report to the Google app indexing API, once, when the fragment is no longer viewed
     */
    private void reportAppIndexEndIfAble() {
        /* If the view was previously reported, and the name exists */
        if (mHasReportedView && mCardName != null) {
            /* Call end() and disconnect the client */
            AppIndexingWrapper.endAppIndexing(getFamiliarActivity().mAppIndexingWrapper, this);
            getFamiliarActivity().mAppIndexingWrapper.disconnect();

            /* manage state */
            mHasReportedView = false;
        }
    }

    /**
     * Set a hint to the system about whether this fragment's UI is currently visible to the user.
     * This hint defaults to true and is persistent across fragment instance state save and restore.
     * <p/>
     * An app may set this to false to indicate that the fragment's UI is scrolled out of visibility
     * or is otherwise not directly visible to the user. This may be used by the system to
     * prioritize operations such as fragment lifecycle updates or loader ordering behavior.
     * <p/>
     * In this case, it's used to report fragment views to Google app indexing
     *
     * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
     *                        false if it is not.
     */
    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
        super.setUserVisibleHint(isVisibleToUser);
        if (isVisibleToUser) {
            /* If the fragment is visible to the user, attempt to report the view */
            reportAppIndexViewIfAble();
        } else {
            /* The view isn't visible anymore, attempt to report it */
            reportAppIndexEndIfAble();
        }
    }

    /**
     * Inflates the view and saves references to UI elements for later filling
     *
     * @param savedInstanceState If non-null, this fragment is being re-constructed from a previous saved state as given
     *                           here.
     * @param inflater           The LayoutInflater object that can be used to inflate any views in the fragment,
     * @param container          If non-null, this is the parent view that the fragment's UI should be attached to. The
     *                           fragment should not add the view itself, but this can be used to generate the
     *                           LayoutParams of the view.
     * @return The inflated view
     */
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

        try {
            mActivity = ((FamiliarFragment) getParentFragment()).getFamiliarActivity();
        } catch (NullPointerException e) {
            mActivity = getFamiliarActivity();
        }

        View myFragmentView = inflater.inflate(R.layout.card_view_frag, container, false);

        assert myFragmentView != null; /* Because Android Studio */
        mNameTextView = (TextView) myFragmentView.findViewById(R.id.name);
        mCostTextView = (TextView) myFragmentView.findViewById(R.id.cost);
        mTypeTextView = (TextView) myFragmentView.findViewById(R.id.type);
        mSetTextView = (TextView) myFragmentView.findViewById(R.id.set);
        mAbilityTextView = (TextView) myFragmentView.findViewById(R.id.ability);
        mFlavorTextView = (TextView) myFragmentView.findViewById(R.id.flavor);
        mArtistTextView = (TextView) myFragmentView.findViewById(R.id.artist);
        mNumberTextView = (TextView) myFragmentView.findViewById(R.id.number);
        mPowTouTextView = (TextView) myFragmentView.findViewById(R.id.pt);
        mTransformButtonDivider = myFragmentView.findViewById(R.id.transform_button_divider);
        mTransformButton = (Button) myFragmentView.findViewById(R.id.transformbutton);
        mTextScrollView = (ScrollView) myFragmentView.findViewById(R.id.cardTextScrollView);
        mImageScrollView = (ScrollView) myFragmentView.findViewById(R.id.cardImageScrollView);
        mCardImageView = (ImageView) myFragmentView.findViewById(R.id.cardpic);
        mColorIndicatorLayout = (LinearLayout) myFragmentView.findViewById(R.id.color_indicator_view);

        registerForContextMenu(mNameTextView);
        registerForContextMenu(mCostTextView);
        registerForContextMenu(mTypeTextView);
        registerForContextMenu(mSetTextView);
        registerForContextMenu(mAbilityTextView);
        registerForContextMenu(mPowTouTextView);
        registerForContextMenu(mFlavorTextView);
        registerForContextMenu(mArtistTextView);
        registerForContextMenu(mNumberTextView);

        mSetTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                SearchCriteria setSearch = new SearchCriteria();
                assert mSetTextView.getText() != null;
                setSearch.set = mSetTextView.getText().toString();
                Bundle arguments = new Bundle();
                arguments.putSerializable(SearchViewFragment.CRITERIA, setSearch);
                ResultListFragment rlFrag = new ResultListFragment();
                startNewFragment(rlFrag, arguments);
            }
        });

        mCardImageView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                if (mAsyncTask != null) {
                    mAsyncTask.cancel(true);
                }
                mAsyncTask = new saveCardImageTask();
                ((saveCardImageTask) mAsyncTask).execute(MAIN_PAGE);
                return true;
            }
        });

        setInfoFromBundle(this.getArguments());

        return myFragmentView;
    }

    /**
     * When the view is destroyed, release any memory used to display card images
     */
    @Override
    public void onDestroyView() {
        super.onDestroyView();
        releaseImageResources(false);
    }

    /**
     * Release all image resources and invoke the garbage collector
     */
    private void releaseImageResources(boolean isSplit) {

        if (mCardImageView != null) {

            /* Release the drawable from the ImageView */
            Drawable drawable = mCardImageView.getDrawable();
            if (drawable != null) {
                drawable.setCallback(null);
                Bitmap drawableBitmap = ((BitmapDrawable) drawable).getBitmap();
                if (drawableBitmap != null) {
                    drawableBitmap.recycle();
                }
            }

            /* Release the ImageView */
            mCardImageView.setImageDrawable(null);
            mCardImageView.setImageBitmap(null);

            if (!isSplit) {
                mCardImageView = null;
            }
        }
        if (mCardBitmap != null) {
            /* Release the drawable */
            mCardBitmap.getBitmap().recycle();
            mCardBitmap = null;
        }

        if (!isSplit) {
            mNameTextView = null;
            mCostTextView = null;
            mTypeTextView = null;
            mSetTextView = null;
            mAbilityTextView = null;
            mFlavorTextView = null;
            mArtistTextView = null;
            mNumberTextView = null;
            mPowTouTextView = null;
            mTransformButtonDivider = null;
            mTransformButton = null;
            mTextScrollView = null;
            mImageScrollView = null;
            mCardImageView = null;
            mColorIndicatorLayout = null;
        }

        /* Invoke the garbage collector */
        java.lang.System.gc();
    }

    /**
     * This will fill the UI elements with database information about the card specified in the given bundle
     *
     * @param extras The bundle passed to this fragment
     */
    private void setInfoFromBundle(Bundle extras) {
        if (extras == null && mNameTextView != null) {
            mNameTextView.setText("");
            mCostTextView.setText("");
            mTypeTextView.setText("");
            mSetTextView.setText("");
            mAbilityTextView.setText("");
            mFlavorTextView.setText("");
            mArtistTextView.setText("");
            mNumberTextView.setText("");
            mPowTouTextView.setText("");
            mTransformButton.setVisibility(View.GONE);
            mTransformButtonDivider.setVisibility(View.GONE);
        } else if (extras != null) {
            long cardID = extras.getLong(CARD_ID);

            /* from onCreateView */
            setInfoFromID(cardID);
        }
    }

    /**
     * This will fill the UI elements with information from the database
     * It also saves information for AsyncTasks to use later and manages the transform/flip button
     *
     * @param id the ID of the the card to be displayed
     * @return true if the UI was filled in, false otherwise
     */
    public void setInfoFromID(final long id) {

        /* If the views are null, don't attempt to fill them in */
        if (mSetTextView == null) {
            return;
        }

        ImageGetter imgGetter = ImageGetterHelper.GlyphGetter(getActivity());

        SQLiteDatabase database = DatabaseManager.getInstance(getActivity(), false).openDatabase(false);
        Cursor cCardById;
        try {
            cCardById = CardDbAdapter.fetchCards(new long[] { id }, null, database);
        } catch (FamiliarDbException e) {
            handleFamiliarDbException(true);
            DatabaseManager.getInstance(getActivity(), false).closeDatabase(false);
            return;
        }

        /* http://magiccards.info/scans/en/mt/55.jpg */
        mCardName = cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_NAME));
        mCardCMC = cCardById.getInt(cCardById.getColumnIndex(CardDbAdapter.KEY_CMC));
        mSetCode = cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_SET));

        /* Start building a description */
        addToDescription(getString(R.string.search_name), mCardName);
        try {
            mSetName = CardDbAdapter.getSetNameFromCode(mSetCode, database);
            addToDescription(getString(R.string.search_set), mSetName);
        } catch (FamiliarDbException e) {
            /* no set for you */
        }

        try {
            mMagicCardsInfoSetCode = CardDbAdapter
                    .getCodeMtgi(cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_SET)), database);
        } catch (FamiliarDbException e) {
            handleFamiliarDbException(true);
            DatabaseManager.getInstance(getActivity(), false).closeDatabase(false);
            return;
        }
        mCardNumber = cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_NUMBER));

        switch ((char) cCardById.getInt(cCardById.getColumnIndex(CardDbAdapter.KEY_RARITY))) {
        case 'C':
        case 'c':
            mSetTextView
                    .setTextColor(ContextCompat.getColor(getContext(), getResourceIdFromAttr(R.attr.color_common)));
            addToDescription(getString(R.string.search_rarity), getString(R.string.search_Common));
            break;
        case 'U':
        case 'u':
            mSetTextView.setTextColor(
                    ContextCompat.getColor(getContext(), getResourceIdFromAttr(R.attr.color_uncommon)));
            addToDescription(getString(R.string.search_rarity), getString(R.string.search_Uncommon));
            break;
        case 'R':
        case 'r':
            mSetTextView
                    .setTextColor(ContextCompat.getColor(getContext(), getResourceIdFromAttr(R.attr.color_rare)));
            addToDescription(getString(R.string.search_rarity), getString(R.string.search_Rare));
            break;
        case 'M':
        case 'm':
            mSetTextView
                    .setTextColor(ContextCompat.getColor(getContext(), getResourceIdFromAttr(R.attr.color_mythic)));
            addToDescription(getString(R.string.search_rarity), getString(R.string.search_Mythic));
            break;
        case 'T':
        case 't':
            mSetTextView.setTextColor(
                    ContextCompat.getColor(getContext(), getResourceIdFromAttr(R.attr.color_timeshifted)));
            addToDescription(getString(R.string.search_rarity), getString(R.string.search_Timeshifted));
            break;
        }

        String sCost = cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_MANACOST));
        addToDescription(getString(R.string.search_mana_cost), sCost);
        CharSequence csCost = ImageGetterHelper.formatStringWithGlyphs(sCost, imgGetter);
        mCostTextView.setText(csCost);

        String sAbility = cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_ABILITY));
        addToDescription(getString(R.string.search_text), sAbility);
        CharSequence csAbility = ImageGetterHelper.formatStringWithGlyphs(sAbility, imgGetter);
        mAbilityTextView.setText(csAbility);
        mAbilityTextView.setMovementMethod(LinkMovementMethod.getInstance());

        String sFlavor = cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_FLAVOR));
        addToDescription(getString(R.string.search_flavor_text), sFlavor);
        CharSequence csFlavor = ImageGetterHelper.formatStringWithGlyphs(sFlavor, imgGetter);
        mFlavorTextView.setText(csFlavor);

        mNameTextView.setText(cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_NAME)));
        mCardType = CardDbAdapter.getTypeLine(cCardById);
        mTypeTextView.setText(mCardType);
        mSetTextView.setText(cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_SET)));
        mArtistTextView.setText(cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_ARTIST)));
        String numberAndRarity = cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_NUMBER)) + " ("
                + (char) cCardById.getInt(cCardById.getColumnIndex(CardDbAdapter.KEY_RARITY)) + ")";
        mNumberTextView.setText(numberAndRarity);

        addToDescription(getString(R.string.search_type), CardDbAdapter.getTypeLine(cCardById));
        addToDescription(getString(R.string.search_artist),
                cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_ARTIST)));
        addToDescription(getString(R.string.search_collectors_number),
                cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_NUMBER)));

        int loyalty = cCardById.getInt(cCardById.getColumnIndex(CardDbAdapter.KEY_LOYALTY));
        float p = cCardById.getFloat(cCardById.getColumnIndex(CardDbAdapter.KEY_POWER));
        float t = cCardById.getFloat(cCardById.getColumnIndex(CardDbAdapter.KEY_TOUGHNESS));
        if (loyalty != CardDbAdapter.NO_ONE_CARES) {
            if (loyalty == CardDbAdapter.X) {
                mPowTouTextView.setText("X");
            } else {
                mPowTouTextView.setText(Integer.valueOf(loyalty).toString());
            }
        } else if (p != CardDbAdapter.NO_ONE_CARES && t != CardDbAdapter.NO_ONE_CARES) {

            String powTouStr = "";

            if (p == CardDbAdapter.STAR)
                powTouStr += "*";
            else if (p == CardDbAdapter.ONE_PLUS_STAR)
                powTouStr += "1+*";
            else if (p == CardDbAdapter.TWO_PLUS_STAR)
                powTouStr += "2+*";
            else if (p == CardDbAdapter.SEVEN_MINUS_STAR)
                powTouStr += "7-*";
            else if (p == CardDbAdapter.STAR_SQUARED)
                powTouStr += "*^2";
            else if (p == CardDbAdapter.X)
                powTouStr += "X";
            else {
                if (p == (int) p) {
                    powTouStr += (int) p;
                } else {
                    powTouStr += p;
                }
            }

            powTouStr += "/";

            if (t == CardDbAdapter.STAR)
                powTouStr += "*";
            else if (t == CardDbAdapter.ONE_PLUS_STAR)
                powTouStr += "1+*";
            else if (t == CardDbAdapter.TWO_PLUS_STAR)
                powTouStr += "2+*";
            else if (t == CardDbAdapter.SEVEN_MINUS_STAR)
                powTouStr += "7-*";
            else if (t == CardDbAdapter.STAR_SQUARED)
                powTouStr += "*^2";
            else if (t == CardDbAdapter.X)
                powTouStr += "X";
            else {
                if (t == (int) t) {
                    powTouStr += (int) t;
                } else {
                    powTouStr += t;
                }
            }

            addToDescription(getString(R.string.search_power), powTouStr);

            mPowTouTextView.setText(powTouStr);
        } else {
            mPowTouTextView.setText("");
        }

        boolean isMultiCard = false;
        switch (CardDbAdapter.isMultiCard(mCardNumber,
                cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_SET)))) {
        case NOPE:
            isMultiCard = false;
            mTransformButton.setVisibility(View.GONE);
            mTransformButtonDivider.setVisibility(View.GONE);
            break;
        case TRANSFORM:
            isMultiCard = true;
            mTransformButton.setVisibility(View.VISIBLE);
            mTransformButtonDivider.setVisibility(View.VISIBLE);
            mTransformButton.setText(R.string.card_view_transform);
            break;
        case FUSE:
            isMultiCard = true;
            mTransformButton.setVisibility(View.VISIBLE);
            mTransformButtonDivider.setVisibility(View.VISIBLE);
            mTransformButton.setText(R.string.card_view_fuse);
            break;
        case SPLIT:
            isMultiCard = true;
            mTransformButton.setVisibility(View.VISIBLE);
            mTransformButtonDivider.setVisibility(View.VISIBLE);
            mTransformButton.setText(R.string.card_view_other_half);
            break;
        }

        if (isMultiCard) {
            if (mCardNumber.contains("a")) {
                mTransformCardNumber = mCardNumber.replace("a", "b");
            } else if (mCardNumber.contains("b")) {
                mTransformCardNumber = mCardNumber.replace("b", "a");
            }
            try {
                mTransformId = CardDbAdapter.getIdFromSetAndNumber(mSetCode, mTransformCardNumber, database);
            } catch (FamiliarDbException e) {
                handleFamiliarDbException(true);
                DatabaseManager.getInstance(getActivity(), false).closeDatabase(false);
                return;
            }
            if (mTransformId == -1) {
                mTransformButton.setVisibility(View.GONE);
                mTransformButtonDivider.setVisibility(View.GONE);
            } else {
                mTransformButton.setOnClickListener(new View.OnClickListener() {
                    public void onClick(View v) {
                        releaseImageResources(true);
                        mCardNumber = mTransformCardNumber;
                        setInfoFromID(mTransformId);
                    }
                });
            }
        }

        mMultiverseId = cCardById.getInt(cCardById.getColumnIndex(CardDbAdapter.KEY_MULTIVERSEID));

        /* Do we load the image immediately to the main page, or do it in a dialog later? */
        if (mActivity.mPreferenceAdapter.getPicFirst()) {
            mImageScrollView.setVisibility(View.VISIBLE);
            mTextScrollView.setVisibility(View.GONE);

            mActivity.setLoading();
            if (mAsyncTask != null) {
                mAsyncTask.cancel(true);
            }
            mAsyncTask = new FetchPictureTask();
            ((FetchPictureTask) mAsyncTask).execute(MAIN_PAGE);
        } else {
            mImageScrollView.setVisibility(View.GONE);
            mTextScrollView.setVisibility(View.VISIBLE);
        }

        /* Figure out how large the color indicator should be. Medium text is 18sp, with a border its 22sp */
        int dimension = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 22,
                getResources().getDisplayMetrics());

        mColorIndicatorLayout.removeAllViews();
        ColorIndicatorView civ = new ColorIndicatorView(this.getActivity(), dimension, dimension / 15,
                cCardById.getString(cCardById.getColumnIndex(CardDbAdapter.KEY_COLOR)), sCost);
        if (civ.shouldInidcatorBeShown()) {
            mColorIndicatorLayout.setVisibility(View.VISIBLE);
            mColorIndicatorLayout.addView(civ);
        } else {
            mColorIndicatorLayout.setVisibility(View.GONE);
        }

        String allLanguageKeys[][] = {
                { Language.Chinese_Traditional, CardDbAdapter.KEY_NAME_CHINESE_TRADITIONAL,
                        CardDbAdapter.KEY_MULTIVERSEID_CHINESE_TRADITIONAL },
                { Language.Chinese_Simplified, CardDbAdapter.KEY_NAME_CHINESE_SIMPLIFIED,
                        CardDbAdapter.KEY_MULTIVERSEID_CHINESE_SIMPLIFIED },
                { Language.French, CardDbAdapter.KEY_NAME_FRENCH, CardDbAdapter.KEY_MULTIVERSEID_FRENCH },
                { Language.German, CardDbAdapter.KEY_NAME_GERMAN, CardDbAdapter.KEY_MULTIVERSEID_GERMAN },
                { Language.Italian, CardDbAdapter.KEY_NAME_ITALIAN, CardDbAdapter.KEY_MULTIVERSEID_ITALIAN },
                { Language.Japanese, CardDbAdapter.KEY_NAME_JAPANESE, CardDbAdapter.KEY_MULTIVERSEID_JAPANESE },
                { Language.Portuguese_Brazil, CardDbAdapter.KEY_NAME_PORTUGUESE_BRAZIL,
                        CardDbAdapter.KEY_MULTIVERSEID_PORTUGUESE_BRAZIL },
                { Language.Russian, CardDbAdapter.KEY_NAME_RUSSIAN, CardDbAdapter.KEY_MULTIVERSEID_RUSSIAN },
                { Language.Spanish, CardDbAdapter.KEY_NAME_SPANISH, CardDbAdapter.KEY_MULTIVERSEID_SPANISH },
                { Language.Korean, CardDbAdapter.KEY_NAME_KOREAN, CardDbAdapter.KEY_MULTIVERSEID_KOREAN } };

        // Clear the translations first
        mTranslatedNames.clear();

        // Add English
        Card.ForeignPrinting englishPrinting = new Card.ForeignPrinting();
        englishPrinting.mLanguageCode = Language.English;
        englishPrinting.mName = mCardName;
        englishPrinting.mMultiverseId = mMultiverseId;
        mTranslatedNames.add(englishPrinting);

        // Add all the others
        for (String lang[] : allLanguageKeys) {
            Card.ForeignPrinting fp = new Card.ForeignPrinting();
            fp.mLanguageCode = lang[0];
            fp.mName = cCardById.getString(cCardById.getColumnIndex(lang[1]));
            fp.mMultiverseId = cCardById.getInt(cCardById.getColumnIndex(lang[2]));
            if (fp.mName != null && !fp.mName.isEmpty()) {
                mTranslatedNames.add(fp);
            }
        }

        cCardById.close();

        /* Find the other sets this card is in ahead of time, so that it can be remove from the menu if there is only
           one set */
        Cursor cCardByName;
        try {
            cCardByName = CardDbAdapter.fetchCardByName(mCardName,
                    new String[] { CardDbAdapter.DATABASE_TABLE_CARDS + "." + CardDbAdapter.KEY_SET,
                            CardDbAdapter.DATABASE_TABLE_CARDS + "." + CardDbAdapter.KEY_ID,
                            CardDbAdapter.DATABASE_TABLE_CARDS + "." + CardDbAdapter.KEY_NUMBER },
                    false, database);
        } catch (FamiliarDbException e) {
            handleFamiliarDbException(true);
            DatabaseManager.getInstance(getActivity(), false).closeDatabase(false);
            return;
        }
        mPrintings = new LinkedHashSet<>();
        mCardIds = new LinkedHashSet<>();
        while (!cCardByName.isAfterLast()) {
            try {
                String number = cCardByName.getString(cCardByName.getColumnIndex(CardDbAdapter.KEY_NUMBER));
                if (!(number == null || number.length() == 0)) {
                    number = " (" + number + ")";
                } else {
                    number = "";
                }
                if (mPrintings.add(CardDbAdapter.getSetNameFromCode(
                        cCardByName.getString(cCardByName.getColumnIndex(CardDbAdapter.KEY_SET)), database)
                        + number)) {
                    mCardIds.add(cCardByName.getLong(cCardByName.getColumnIndex(CardDbAdapter.KEY_ID)));
                }
            } catch (FamiliarDbException e) {
                handleFamiliarDbException(true);
                DatabaseManager.getInstance(getActivity(), false).closeDatabase(false);
                return;
            }
            cCardByName.moveToNext();
        }
        cCardByName.close();
        /* If it exists in only one set, remove the button from the menu */
        if (mPrintings.size() == 1) {
            mActivity.supportInvalidateOptionsMenu();
        }
        DatabaseManager.getInstance(getActivity(), false).closeDatabase(false);

        if (mShouldReportView) {
            reportAppIndexViewIfAble();
        }
    }

    /**
     * Used to build a meta description of this card, for app indexing
     *
     * @param tag  A tag for this data
     * @param data The data to add to the description
     */
    private void addToDescription(String tag, String data) {
        if (mDescription == null) {
            mDescription = tag + ": \"" + data + "\"";
        } else {
            mDescription += "\n" + tag + ": \"" + data + "\"";
        }
    }

    /**
     * Remove any showing dialogs, and show the requested one
     *
     * @param id the ID of the dialog to show
     */
    private void showDialog(final int id) throws IllegalStateException {
        /* DialogFragment.show() will take care of adding the fragment in a transaction. We also want to remove any
        currently showing dialog, so make our own transaction and take care of that here. */

        /* If the fragment isn't visible (maybe being loaded by the pager), don't show dialogs */
        if (!this.isVisible()) {
            return;
        }

        removeDialog(getFragmentManager());

        /* Create and show the dialog. */
        CardViewDialogFragment newFragment = new CardViewDialogFragment();
        Bundle arguments = new Bundle();
        arguments.putInt(FamiliarDialogFragment.ID_KEY, id);
        newFragment.setArguments(arguments);
        newFragment.show(getFragmentManager(), FamiliarActivity.DIALOG_TAG);
    }

    /**
     * Called when a registered view is long-pressed. The menu inflated will give different options based on the view class
     *
     * @param menu     The context menu that is being built
     * @param v        The view for which the context menu is being built
     * @param menuInfo Extra information about the item for which the context menu should be shown. This information
     *                 will vary depending on the class of v.
     */
    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {

        super.onCreateContextMenu(menu, v, menuInfo);

        TextView tv = (TextView) v;

        assert tv.getText() != null;
        mCopyString = tv.getText().toString();

        android.view.MenuInflater inflater = this.mActivity.getMenuInflater();
        inflater.inflate(R.menu.copy_menu, menu);
    }

    /**
     * Copies text to the clipboard
     *
     * @param item The context menu item that was selected.
     * @return boolean Return false to allow normal context menu processing to proceed, true to consume it here.
     */
    @Override
    public boolean onContextItemSelected(android.view.MenuItem item) {
        if (getUserVisibleHint()) {
            String copyText = null;
            switch (item.getItemId()) {
            case R.id.copy: {
                copyText = mCopyString;
                break;
            }
            case R.id.copyall: {
                if (mNameTextView.getText() != null && mCostTextView.getText() != null
                        && mTypeTextView.getText() != null && mSetTextView.getText() != null
                        && mAbilityTextView.getText() != null && mFlavorTextView.getText() != null
                        && mPowTouTextView.getText() != null && mArtistTextView.getText() != null
                        && mNumberTextView.getText() != null) {
                    // Hacky, but it works
                    String costText = convertHtmlToPlainText(
                            Html.toHtml(new SpannableString(mCostTextView.getText())));
                    String abilityText = convertHtmlToPlainText(
                            Html.toHtml(new SpannableString(mAbilityTextView.getText())));
                    copyText = mNameTextView.getText().toString() + '\n' + costText + '\n'
                            + mTypeTextView.getText().toString() + '\n' + mSetTextView.getText().toString() + '\n'
                            + abilityText + '\n' + mFlavorTextView.getText().toString() + '\n'
                            + mPowTouTextView.getText().toString() + '\n' + mArtistTextView.getText().toString()
                            + '\n' + mNumberTextView.getText().toString();
                }
                break;
            }
            default: {
                return super.onContextItemSelected(item);
            }
            }

            if (copyText != null) {
                ClipboardManager clipboard = (ClipboardManager) (this.mActivity
                        .getSystemService(android.content.Context.CLIPBOARD_SERVICE));
                String label = getResources().getString(R.string.app_name);
                String mimeTypes[] = { ClipDescription.MIMETYPE_TEXT_PLAIN };
                ClipData cd = new ClipData(label, mimeTypes, new ClipData.Item(copyText));
                clipboard.setPrimaryClip(cd);
            }
            return true;
        }
        return false;
    }

    /**
     * Converts some html to plain text, replacing images with their textual counterparts
     *
     * @param html html to be converted
     * @return plain text representation of the input
     */
    public String convertHtmlToPlainText(String html) {
        Document document = Jsoup.parse(html);
        Elements images = document.select("img");
        for (Element image : images) {
            image.html("{" + image.attr("src") + "}");
        }
        return document.text();
    }

    /**
     * Handles clicks from the ActionBar
     *
     * @param item the item clicked
     * @return true if acted upon, false if otherwise
     */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (mCardName == null) {
            /*disable menu buttons if the card isn't initialized */
            return false;
        }
        /* Handle item selection */
        switch (item.getItemId()) {
        case R.id.image: {
            if (FamiliarActivity.getNetworkState(getContext(), true) == -1) {
                return true;
            }

            mActivity.setLoading();
            if (mAsyncTask != null) {
                mAsyncTask.cancel(true);
            }
            mAsyncTask = new FetchPictureTask();
            ((FetchPictureTask) mAsyncTask).execute(DIALOG);
            return true;
        }
        case R.id.price: {
            mActivity.setLoading();

            PriceFetchRequest priceRequest;
            priceRequest = new PriceFetchRequest(mCardName, mSetCode, mCardNumber, mMultiverseId, getActivity());
            mActivity.mSpiceManager.execute(priceRequest, mCardName + "-" + mSetCode, DurationInMillis.ONE_DAY,
                    new RequestListener<PriceInfo>() {

                        @Override
                        public void onRequestFailure(SpiceException spiceException) {
                            if (CardViewFragment.this.isAdded()) {
                                mActivity.clearLoading();

                                CardViewFragment.this.removeDialog(getFragmentManager());
                                ToastWrapper
                                        .makeText(mActivity, spiceException.getMessage(), ToastWrapper.LENGTH_SHORT)
                                        .show();
                            }
                        }

                        @Override
                        public void onRequestSuccess(final PriceInfo result) {
                            if (CardViewFragment.this.isAdded()) {
                                mActivity.clearLoading();

                                if (result != null) {
                                    mPriceInfo = result;
                                    showDialog(CardViewDialogFragment.GET_PRICE);
                                } else {
                                    ToastWrapper.makeText(mActivity, R.string.card_view_price_not_found,
                                            ToastWrapper.LENGTH_SHORT).show();
                                }
                            }
                        }
                    });

            return true;
        }
        case R.id.changeset: {
            showDialog(CardViewDialogFragment.CHANGE_SET);
            return true;
        }
        case R.id.legality: {
            mActivity.setLoading();
            if (mAsyncTask != null) {
                mAsyncTask.cancel(true);
            }
            mAsyncTask = new FetchLegalityTask();
            ((FetchLegalityTask) mAsyncTask).execute((Void[]) null);
            return true;
        }
        case R.id.cardrulings: {
            if (FamiliarActivity.getNetworkState(getContext(), true) == -1) {
                return true;
            }

            mActivity.setLoading();
            if (mAsyncTask != null) {
                mAsyncTask.cancel(true);
            }
            mAsyncTask = new FetchRulingsTask();
            ((FetchRulingsTask) mAsyncTask).execute((Void[]) null);
            return true;
        }
        case R.id.addtowishlist: {
            showDialog(CardViewDialogFragment.WISH_LIST_COUNTS);
            return true;
        }
        case R.id.sharecard: {
            showDialog(CardViewDialogFragment.SHARE_CARD);
            return true;
        }
        case R.id.translatecard: {
            showDialog(CardViewDialogFragment.TRANSLATE_CARD);
            return true;
        }
        default: {
            return super.onOptionsItemSelected(item);
        }
        }
    }

    /**
     * Inflate the ActionBar items
     *
     * @param menu     The options menu in which you place your items.
     * @param inflater The inflater to use to inflate the menu
     */
    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        super.onCreateOptionsMenu(menu, inflater);
        inflater.inflate(R.menu.card_menu, menu);
    }

    /**
     * Prepare the Screen's standard options menu to be displayed.  This is
     * called right before the menu is shown, every time it is shown.  You can
     * use this method to efficiently enable/disable items or otherwise
     * dynamically modify the contents.
     *
     * @param menu The options menu as last shown or first initialized by
     *             onCreateOptionsMenu().
     * @see #setHasOptionsMenu
     * @see #onCreateOptionsMenu
     */
    @Override
    public void onPrepareOptionsMenu(Menu menu) {
        super.onPrepareOptionsMenu(menu);

        MenuItem mi;
        /* If the image has been loaded to the main page, remove the menu option for image */
        if (mActivity.mPreferenceAdapter.getPicFirst() && mCardBitmap != null) {
            mi = menu.findItem(R.id.image);
            if (mi != null) {
                menu.removeItem(mi.getItemId());
            }
        }
        /* This code removes the "change set" button if there is only one set.
         * Turns out some users use it to view the full set name when there is only one set/
         * I'm leaving it here, but commented, for posterity */
        /*
         if (mPrintings != null && mPrintings.size() == 1) {
        mi = menu.findItem(R.id.changeset);
        if (mi != null) {
            menu.removeItem(mi.getItemId());
        }
        }
        */
    }

    /**
     * Called from the share dialog to load and share this card's image
     */
    public void runShareImageTask() {
        mActivity.setLoading();
        if (mAsyncTask != null) {
            mAsyncTask.cancel(true);
        }
        mAsyncTask = new FetchPictureTask();
        ((FetchPictureTask) mAsyncTask).execute(SHARE);
    }

    /**
     * This inner class encapsulates a ruling and the date it was made
     */
    public static class Ruling {
        public final String date;
        public final String ruling;

        public Ruling(String d, String r) {
            date = d;
            ruling = r;
        }

        public String toString() {
            return date + ": " + ruling;
        }
    }

    public class saveCardImageTask extends AsyncTask<Integer, Void, Void> {

        String mToastString = null;
        private Integer mWhereTo;

        @Override
        protected Void doInBackground(Integer... params) {

            if (params != null && params.length > 0) {
                mWhereTo = params[0];
            } else {
                mWhereTo = MAIN_PAGE;
            }

            if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                mToastString = getString(R.string.card_view_no_external_storage);
                return null;
            }

            /* Check if permission is granted */
            if (ContextCompat.checkSelfPermission(CardViewFragment.this.mActivity,
                    Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
                /* Request the permission */
                ActivityCompat.requestPermissions(CardViewFragment.this.mActivity,
                        new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE },
                        FamiliarActivity.MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
            } else {
                /* Permission already granted */
                mToastString = saveImage();
            }

            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            if (mWhereTo == SHARE) {
                try {

                    /* Start the intent to share the image */
                    Uri uri = FileProvider.getUriForFile(mActivity, "com.gelakinetic.mtgfam.FileProvider",
                            getSavedImageFile(false));
                    Intent shareIntent = new Intent();
                    shareIntent.setAction(Intent.ACTION_SEND);
                    shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
                    shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
                    shareIntent.setType("image/jpeg");
                    startActivity(
                            Intent.createChooser(shareIntent, getResources().getText(R.string.card_view_send_to)));

                } catch (Exception e) {
                    ToastWrapper.makeText(mActivity, e.getMessage(), ToastWrapper.LENGTH_LONG).show();
                }
            } else if (mToastString != null) {
                ToastWrapper.makeText(mActivity, mToastString, ToastWrapper.LENGTH_LONG).show();
            }
        }
    }

    /**
     * This private class handles asking the database about the legality of a card, and will eventually show the
     * information in a Dialog
     */
    private class FetchLegalityTask extends AsyncTask<Void, Void, Void> {

        /**
         * Queries the data in the database to see what sets this card is legal in
         *
         * @param params unused
         * @return unused
         */
        @Override
        protected Void doInBackground(Void... params) {

            SQLiteDatabase database = DatabaseManager.getInstance(getActivity(), false).openDatabase(false);
            try {
                Cursor cFormats = CardDbAdapter.fetchAllFormats(database);
                mFormats = new String[cFormats.getCount()];
                mLegalities = new String[cFormats.getCount()];

                cFormats.moveToFirst();
                for (int i = 0; i < cFormats.getCount(); i++) {
                    mFormats[i] = cFormats.getString(cFormats.getColumnIndex(CardDbAdapter.KEY_NAME));
                    switch (CardDbAdapter.checkLegality(mCardName, mFormats[i], database)) {
                    case CardDbAdapter.LEGAL:
                        mLegalities[i] = getString(R.string.card_view_legal);
                        break;
                    case CardDbAdapter.RESTRICTED:
                        /* For backwards compatibility, we list cards that are legal
                         * in commander, but can't be the commander as Restricted in
                         * the legality file.  This prevents older version of the app
                         * from throwing an IllegalStateException if we try including
                         * a new legality. */
                        if (mFormats[i].equalsIgnoreCase("Commander")) {
                            mLegalities[i] = getString(R.string.card_view_no_commander);
                        } else {
                            mLegalities[i] = getString(R.string.card_view_restricted);
                        }
                        break;
                    case CardDbAdapter.BANNED:
                        mLegalities[i] = getString(R.string.card_view_banned);
                        break;
                    default:
                        mLegalities[i] = getString(R.string.error);
                        break;
                    }
                    cFormats.moveToNext();
                }
                cFormats.close();
            } catch (FamiliarDbException e) {
                CardViewFragment.this.handleFamiliarDbException(false);
                mLegalities = null;
            }

            DatabaseManager.getInstance(getActivity(), false).closeDatabase(false);
            return null;
        }

        /**
         * After the query, remove the progress dialog and show the legalities
         *
         * @param result unused
         */
        @Override
        protected void onPostExecute(Void result) {
            try {
                showDialog(CardViewDialogFragment.GET_LEGALITY);
            } catch (IllegalStateException e) {
                /* eat it */
            }
            mActivity.clearLoading();
        }
    }

    /**
     * This private class retrieves a picture of the card from the internet
     */
    private class FetchPictureTask extends AsyncTask<Integer, Void, Void> {

        int mHeight;
        int mWidth;
        int mBorder;

        private String mError;
        private int mLoadTo;
        private String mImageKey;

        /* Get the size of the window on the UI thread, not the worker thread */
        final Runnable getWindowSize = new Runnable() {
            @Override
            public void run() {
                Rect rectangle = new Rect();
                mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(rectangle);

                assert mActivity.getSupportActionBar() != null; /* Because Android Studio */
                mHeight = ((rectangle.bottom - rectangle.top) - mActivity.getSupportActionBar().getHeight())
                        - mBorder;
                mWidth = (rectangle.right - rectangle.left) - mBorder;

                synchronized (this) {
                    this.notify();
                }
            }
        };

        /**
         * If the preferred langauge is English, get the card image from Scryfall
         * If that fails, check www.MagicCards.info for the card image in the user's preferred language
         * If that fails, try Scryfall again in English
         * If that fails, check www.MagicCards.info for the card image in English
         * If that fails, check www.gatherer.wizards.com for the card image
         * If that fials, give up
         * There is a non-standard URL building for planes and schemes for www.MagicCards.info
         * It also re-sizes the image
         *
         * @param params unused
         * @return unused
         */
        @SuppressWarnings("SpellCheckingInspection")
        @Override
        protected Void doInBackground(Integer... params) {

            if (params != null && params.length > 0) {
                mLoadTo = params[0];
            } else {
                mLoadTo = MAIN_PAGE;
            }

            String cardLanguage = mActivity.mPreferenceAdapter.getCardLanguage();
            if (cardLanguage == null) {
                cardLanguage = "en";
            }

            mImageKey = Integer.toString(mMultiverseId) + cardLanguage;

            /* Check disk cache in background thread */
            Bitmap bitmap;
            try {
                bitmap = getFamiliarActivity().mImageCache.getBitmapFromDiskCache(mImageKey);
            } catch (NullPointerException e) {
                bitmap = null;
            }

            if (bitmap == null) { /* Not found in disk cache */

                /* Some trickery to figure out if we have a token */
                boolean isToken = false;
                if (mCardType.contains("Token") || /* try to take the easy way out */
                        (mCardCMC == 0 && /* Tokens have a CMC of 0 */
                                mSetName.contains("Duel Decks")
                                && /* The only tokens in Gatherer are from Duel Decks */
                                mCardType.contains("Creature"))) { /* The only tokens in Gatherer are creatures */
                    isToken = true;
                }

                boolean bRetry = true;

                boolean triedMtgi = false;
                boolean triedGatherer = false;
                boolean triedScryfall = false;

                while (bRetry) {

                    bRetry = false;
                    mError = null;

                    try {
                        URL u;
                        if (!cardLanguage.equalsIgnoreCase("en") && !isToken) {
                            /* Non-English have to come from magiccards.info. Try there first */
                            u = new URL(
                                    getMtgiPicUrl(mCardName, mMagicCardsInfoSetCode, mCardNumber, cardLanguage));
                            /* If this fails, try next time with the English version */
                            cardLanguage = "en";
                        } else if (!triedScryfall && !isToken) {
                            /* Try downloading the image from Scryfall next */
                            u = new URL(getScryfallImageUri(mMultiverseId));
                            /* If this fails, try next time with the Magiccards.info version */
                            triedScryfall = true;
                        } else if (!triedMtgi && !isToken) {
                            /* Try downloading the image from magiccards.info next */
                            u = new URL(
                                    getMtgiPicUrl(mCardName, mMagicCardsInfoSetCode, mCardNumber, cardLanguage));
                            /* If this fails, try next time with the gatherer version */
                            triedMtgi = true;
                        } else {
                            /* Try downloading the image from gatherer */
                            u = new URL("http://gatherer.wizards.com/Handlers/Image.ashx?multiverseid="
                                    + mMultiverseId + "&type=card");
                            /* If this fails, give up */
                            triedGatherer = true;
                        }

                        /* Download the bitmap */
                        bitmap = BitmapFactory.decodeStream(FamiliarActivity.getHttpInputStream(u, null));
                        /* Cache it */
                        getFamiliarActivity().mImageCache.addBitmapToCache(mImageKey,
                                new BitmapDrawable(mActivity.getResources(), bitmap));
                    } catch (Exception e) {
                        /* Something went wrong */
                        try {
                            mError = getString(R.string.card_view_image_not_found);
                        } catch (RuntimeException re) {
                            /* in case the fragment isn't attached to an activity */
                            mError = e.toString();
                        }

                        /* Gatherer is always tried last. If that fails, give up */
                        if (!triedGatherer) {
                            bRetry = true;
                        }
                    }
                }
            }

            /* Image download failed, just return null */
            if (bitmap == null) {
                return null;
            }

            try {
                /* 16dp */
                mBorder = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
                        getResources().getDisplayMetrics());
                if (mLoadTo == MAIN_PAGE) {
                    /* Block the worker thread until the size is figured out */
                    synchronized (getWindowSize) {
                        getActivity().runOnUiThread(getWindowSize);
                        getWindowSize.wait();
                    }
                } else if (mLoadTo == DIALOG) {
                    Display display = ((WindowManager) mActivity.getSystemService(Context.WINDOW_SERVICE))
                            .getDefaultDisplay();
                    Point p = new Point();
                    display.getSize(p);
                    mHeight = p.y - mBorder;
                    mWidth = p.x - mBorder;
                } else if (mLoadTo == SHARE) {
                    /* Don't scale shared images */
                    mWidth = bitmap.getWidth();
                    mHeight = bitmap.getHeight();
                }

                float screenAspectRatio = (float) mHeight / (float) (mWidth);
                float cardAspectRatio = (float) bitmap.getHeight() / (float) bitmap.getWidth();

                float scale;
                if (screenAspectRatio > cardAspectRatio) {
                    scale = (mWidth) / (float) bitmap.getWidth();
                } else {
                    scale = (mHeight) / (float) bitmap.getHeight();
                }

                int newWidth = Math.round(bitmap.getWidth() * scale);
                int newHeight = Math.round(bitmap.getHeight() * scale);

                Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
                mCardBitmap = new RecyclingBitmapDrawable(mActivity.getResources(), scaledBitmap);
            } catch (Exception e) {
                /* Some error resizing. Out of memory? */
            }

            /* Recycle the non-scaled bitmap to avoid memory leaks */
            bitmap.recycle();
            java.lang.System.gc();
            return null;
        }

        /**
         * Jumps through hoops and returns a correctly formatted URL for magiccards.info's image
         *
         * @param cardName              The name of the card
         * @param magicCardsInfoSetCode The set of the card
         * @param cardNumber            The number of the card
         * @param cardLanguage          The language of the card
         * @return a URL to the card's image
         */
        private String getMtgiPicUrl(String cardName, String magicCardsInfoSetCode, String cardNumber,
                String cardLanguage) {
            String picURL;
            if (mCardType.toLowerCase().contains(getString(R.string.search_Ongoing).toLowerCase()) ||
            /* extra space to not confuse with planeswalker */
                    mCardType.toLowerCase().contains(getString(R.string.search_Plane).toLowerCase() + " ")
                    || mCardType.toLowerCase().contains(getString(R.string.search_Phenomenon).toLowerCase())
                    || mCardType.toLowerCase().contains(getString(R.string.search_Scheme).toLowerCase())) {
                switch (mSetCode) {
                case "PC2":
                    picURL = "http://magiccards.info/extras/plane/planechase-2012-edition/" + cardName + ".jpg";
                    picURL = picURL.replace(" ", "-").replace("?", "").replace(",", "").replace("'", "")
                            .replace("!", "");
                    break;
                case "PCH":
                    if (cardName.equalsIgnoreCase("tazeem")) {
                        cardName = "tazeem-release-promo";
                    } else if (cardName.equalsIgnoreCase("celestine reef")) {
                        cardName = "celestine-reef-pre-release-promo";
                    } else if (cardName.equalsIgnoreCase("horizon boughs")) {
                        cardName = "horizon-boughs-gateway-promo";
                    }
                    picURL = "http://magiccards.info/extras/plane/planechase/" + cardName + ".jpg";
                    picURL = picURL.replace(" ", "-").replace("?", "").replace(",", "").replace("'", "")
                            .replace("!", "");
                    break;
                case "ARC":
                    picURL = "http://magiccards.info/extras/scheme/archenemy/" + cardName + ".jpg";
                    picURL = picURL.replace(" ", "-").replace("?", "").replace(",", "").replace("'", "")
                            .replace("!", "");
                    break;
                default:
                    picURL = "http://magiccards.info/scans/" + cardLanguage + "/" + magicCardsInfoSetCode + "/"
                            + cardNumber + ".jpg";
                    break;
                }
            } else {
                picURL = "http://magiccards.info/scans/" + cardLanguage + "/" + magicCardsInfoSetCode + "/"
                        + cardNumber + ".jpg";
            }
            return picURL.toLowerCase(Locale.ENGLISH);
        }

        /**
         * Easily gets the uri for the image for a card by multiverseid
         *
         * @param multiverseId the multiverse id of the card
         * @return uri of the card image
         */
        private String getScryfallImageUri(int multiverseId) {
            return "https://api.scryfall.com/cards/multiverse/" + multiverseId + "?format=image";
        }

        /**
         * When the task has finished, if there was no error, remove the progress dialog and show the image
         * If the image was supposed to load to the main screen, and it failed to load, fall back to text view
         *
         * @param result unused
         */
        @Override
        protected void onPostExecute(Void result) {
            if (mError == null) {
                if (mLoadTo == DIALOG) {
                    try {
                        showDialog(CardViewDialogFragment.GET_IMAGE);
                    } catch (IllegalStateException e) {
                        /* eat it */
                    }
                } else if (mLoadTo == MAIN_PAGE) {
                    removeDialog(getFragmentManager());
                    if (mCardImageView != null) {
                        mCardImageView.setImageDrawable(mCardBitmap);
                    }
                    /* remove the image load button if it is the main page */
                    mActivity.supportInvalidateOptionsMenu();
                } else if (mLoadTo == SHARE) {

                    /* Images must be saved before sharing */
                    if (mAsyncTask != null) {
                        mAsyncTask.cancel(true);
                    }
                    mAsyncTask = new saveCardImageTask();
                    ((saveCardImageTask) mAsyncTask).execute(SHARE);
                }
            } else {
                removeDialog(getFragmentManager());
                if (mLoadTo == MAIN_PAGE && mImageScrollView != null) {
                    mImageScrollView.setVisibility(View.GONE);
                    mTextScrollView.setVisibility(View.VISIBLE);
                }
                ToastWrapper.makeText(mActivity, mError, ToastWrapper.LENGTH_LONG).show();
            }
            mActivity.clearLoading();
        }

        /**
         * If the task is canceled, fall back to text view
         */
        @Override
        protected void onCancelled() {
            if (mLoadTo == MAIN_PAGE && mImageScrollView != null) {
                mImageScrollView.setVisibility(View.GONE);
                mTextScrollView.setVisibility(View.VISIBLE);
            }
        }
    }

    /**
     * This private class fetches rulings about this card from gatherer.wizards.com
     */
    private class FetchRulingsTask extends AsyncTask<Void, Void, Void> {

        String mErrorMessage = null;

        /**
         * This function downloads the source of the gatherer page, scans it for rulings, and stores them for display
         *
         * @param params unused
         * @return unused
         */
        @Override
        @SuppressWarnings("SpellCheckingInspection")
        protected Void doInBackground(Void... params) {

            URL url;
            InputStream is = null;

            mRulingsArrayList = new ArrayList<>();
            try {
                url = new URL("http://gatherer.wizards.com/Pages/Card/Details.aspx?multiverseid=" + mMultiverseId);
                is = FamiliarActivity.getHttpInputStream(url, null);
                if (is == null) {
                    throw new IOException("null stream");
                }

                String gathererPage = IOUtils.toString(is);
                String date;

                Document document = Jsoup.parse(gathererPage);
                Elements rulingTable = document.select("table.rulingsTable > tbody > tr");

                for (Element ruling : rulingTable) {
                    date = ruling.children().get(0).text();
                    Element rulingText = ruling.children().get(1);
                    Elements imageTags = rulingText.getElementsByTag("img");
                    /* For each symbol in the rulings text */
                    for (Element symbol : imageTags) {
                        /* Build the glyph with {, the text between "name=" and "&" and } */
                        String symbolString = "{" + symbol.attr("src").split("name=")[1].split("&")[0] + "}";
                        /* The new "HTML" for the symbols will be {n}, instead of the img tags they were before */
                        symbol.html(symbolString);
                    }
                    Ruling r = new Ruling(date, rulingText.text());
                    mRulingsArrayList.add(r);
                }
            } catch (Exception ioe) {
                mErrorMessage = ioe.getLocalizedMessage();
            } finally {
                try {
                    if (is != null) {
                        is.close();
                    }
                } catch (IOException ioe) {
                    mErrorMessage = ioe.getLocalizedMessage();
                }
            }

            return null;
        }

        /**
         * Hide the progress dialog and show the rulings, if there are no errors
         *
         * @param result unused
         */
        @Override
        protected void onPostExecute(Void result) {

            if (mErrorMessage == null) {
                try {
                    showDialog(CardViewDialogFragment.CARD_RULINGS);
                } catch (IllegalStateException e) {
                    /* eat it */
                }
            } else {
                removeDialog(getFragmentManager());
                ToastWrapper.makeText(mActivity, mErrorMessage, ToastWrapper.LENGTH_SHORT).show();
            }
            mActivity.clearLoading();
        }
    }

    /**
     * Callback for when a permission is requested
     *
     * @param requestCode  The request code passed in requestPermissions(String[], int).
     * @param permissions  The requested permissions. Never null.
     * @param grantResults The grant results for the corresponding permissions which is either
     *                     android.content.pm.PackageManager.PERMISSION_GRANTED or
     *                     android.content.pm.PackageManager.PERMISSION_DENIED. Never null.
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
            @NonNull int[] grantResults) {
        switch (requestCode) {
        case FamiliarActivity.MY_PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: {
            // If request is cancelled, the result arrays are empty.
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED
                    && permissions[0].equals(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
                /* Permission granted, run the task again */
                if (mAsyncTask instanceof saveCardImageTask) {
                    int whereTo = ((saveCardImageTask) mAsyncTask).mWhereTo;
                    mAsyncTask.cancel(true);
                    mAsyncTask = new saveCardImageTask();
                    ((saveCardImageTask) mAsyncTask).execute(whereTo);
                }
            } else {
                /* Permission denied */
                ToastWrapper.makeText(this.getContext(), getString(R.string.card_view_unable_to_save_image),
                        ToastWrapper.LENGTH_LONG).show();
            }
        }
        }
    }

    /**
     * Returns the File used to save this card's image
     *
     * @param shouldDelete true if the file should be deleted before returned, false otherwise
     * @return A File, either with the image already or blank
     * @throws Exception If something goes wrong
     */
    private File getSavedImageFile(boolean shouldDelete) throws Exception {

        String strPath;
        try {
            strPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getCanonicalPath()
                    + "/MTGFamiliar";
        } catch (IOException ex) {
            throw new Exception(getString(R.string.card_view_no_pictures_folder));
        }

        File fPath = new File(strPath);

        if (!fPath.exists()) {
            if (!fPath.mkdir()) {
                throw new Exception(getString(R.string.card_view_unable_to_create_dir));
            }

            if (!fPath.isDirectory()) {
                throw new Exception(getString(R.string.card_view_unable_to_create_dir));
            }
        }

        fPath = new File(strPath, mCardName + "_" + mSetCode + ".jpg");

        if (shouldDelete) {
            if (fPath.exists()) {
                if (!fPath.delete()) {
                    throw new Exception(getString(R.string.card_view_unable_to_create_file));
                }
            }
        }

        return fPath;
    }

    /**
     * Saves the current card image to external storage
     *
     * @return A status string, to be displayed in a toast on the UI thread
     */
    private String saveImage() {
        File fPath;

        try {
            fPath = getSavedImageFile(true);
        } catch (Exception e) {
            return e.getMessage();
        }

        String strPath = fPath.getAbsolutePath();

        if (fPath.exists()) {
            return getString(R.string.card_view_image_saved) + strPath;
        }
        try {
            if (!fPath.createNewFile()) {
                return getString(R.string.card_view_unable_to_create_file);
            }

            FileOutputStream fStream = new FileOutputStream(fPath);

            /* If the card is displayed, there's a real good chance it's cached */
            String cardLanguage = mActivity.mPreferenceAdapter.getCardLanguage();
            if (cardLanguage == null) {
                cardLanguage = "en";
            }
            String imageKey = Integer.toString(mMultiverseId) + cardLanguage;
            Bitmap bmpImage;
            try {
                bmpImage = getFamiliarActivity().mImageCache.getBitmapFromDiskCache(imageKey);
            } catch (NullPointerException e) {
                bmpImage = null;
            }

            /* Check if this is an english only image */
            if (bmpImage == null && !cardLanguage.equalsIgnoreCase("en")) {
                imageKey = Integer.toString(mMultiverseId) + "en";
                try {
                    bmpImage = getFamiliarActivity().mImageCache.getBitmapFromDiskCache(imageKey);
                } catch (NullPointerException e) {
                    bmpImage = null;
                }
            }

            /* nope, not here */
            if (bmpImage == null) {
                return getString(R.string.card_view_no_image);
            }

            boolean bCompressed = bmpImage.compress(Bitmap.CompressFormat.JPEG, 90, fStream);
            fStream.flush();
            fStream.close();

            if (!bCompressed) {
                return getString(R.string.card_view_unable_to_save_image);
            }

        } catch (IOException ex) {
            return getString(R.string.card_view_save_failure);
        }

        /* Notify the system that a new image was saved */
        getFamiliarActivity().sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(fPath)));

        return getString(R.string.card_view_image_saved) + strPath;
    }
}