Java tutorial
/* * Copyright 2017 Adam Feinstein * * This file is part of MTG Familiar. * * 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. * * 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. * * 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.content.Context; import android.content.Intent; import android.os.Bundle; import android.support.v4.content.ContextCompat; 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.widget.ImageView; import android.widget.TextView; import com.gelakinetic.mtgfam.FamiliarActivity; import com.gelakinetic.mtgfam.R; import com.gelakinetic.mtgfam.fragments.dialogs.FamiliarDialogFragment; import com.gelakinetic.mtgfam.fragments.dialogs.SortOrderDialogFragment; import com.gelakinetic.mtgfam.fragments.dialogs.TradeDialogFragment; import com.gelakinetic.mtgfam.helpers.CardDataAdapter; import com.gelakinetic.mtgfam.helpers.CardDataViewHolder; import com.gelakinetic.mtgfam.helpers.CardHelpers; import com.gelakinetic.mtgfam.helpers.MtgCard; import com.gelakinetic.mtgfam.helpers.PreferenceAdapter; import com.gelakinetic.mtgfam.helpers.ToastWrapper; import com.gelakinetic.mtgfam.helpers.database.CardDbAdapter; import com.gelakinetic.mtgfam.helpers.tcgp.MarketPriceInfo; import org.apache.commons.io.IOUtils; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Locale; /** * This class manages trades between two users. Trades can be saved and loaded. */ public class TradeFragment extends FamiliarListFragment { /* Side constants */ public static final int LEFT = 0; public static final int RIGHT = 1; public static final int BOTH = 2; public static final String TRADE_EXTENSION = ".trade"; private static final String AUTOSAVE_NAME = "autosave"; /* Left List and Company */ public ArrayList<MtgCard> mListLeft; /* Right List and Company */ public ArrayList<MtgCard> mListRight; public String mCurrentTrade = ""; private int mOrderAddedIdx = 0; /** * Initialize the view and set up the button actions. * * @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. * @param savedInstanceState If non-null, this fragment is being re-constructed from a previous * saved state as given here. * @return The inflated view */ @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { /* Inflate the view, pull out the UI elements */ View myFragmentView = inflater.inflate(R.layout.trader_frag, container, false); assert myFragmentView != null; mListLeft = new ArrayList<>(); CardDataAdapter listAdapterLeft = new TradeDataAdapter(mListLeft, LEFT); mListRight = new ArrayList<>(); CardDataAdapter listAdapterRight = new TradeDataAdapter(mListRight, RIGHT); /* Call to set up our shared UI elements */ initializeMembers(myFragmentView, new int[] { R.id.tradeListLeft, R.id.tradeListRight }, new CardDataAdapter[] { listAdapterLeft, listAdapterRight }, new int[] { R.id.priceTextLeft, R.id.priceTextRight }, new int[] { R.id.priceDividerLeft, R.id.priceDividerRight }, R.menu.action_mode_menu, null); /* Click listeners to add cards */ myFragmentView.findViewById(R.id.addCardLeft).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { addCardToTrade(LEFT); } }); myFragmentView.findViewById(R.id.addCardRight).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { addCardToTrade(RIGHT); } }); return myFragmentView; } /** * This helper method adds a card to a side of the wishlist from the user input. * * @param side RIGHT or LEFT, depending on which side to add the card to */ private void addCardToTrade(final int side) { if (getCardNameInput() == null || getCardNameInput().length() == 0 || getCardNumberInput() == null || getCardNumberInput().length() == 0) { return; } final String cardName = getCardNameInput().toString(); final int numberOf = Integer.parseInt(getCardNumberInput().toString()); final boolean isFoil = checkboxFoilIsChecked(); final MtgCard card = CardHelpers.makeMtgCard(getContext(), cardName, null, isFoil, numberOf); if (card == null) { return; } card.setIndex(mOrderAddedIdx++); switch (side) { case LEFT: { mListLeft.add(0, card); getCardDataAdapter(LEFT).notifyItemInserted(0); loadPrice(card); break; } case RIGHT: { mListRight.add(0, card); getCardDataAdapter(RIGHT).notifyItemInserted(0); loadPrice(card); break; } default: { return; } } clearCardNameInput(); clearCardNumberInput(); uncheckFoilCheckbox(); sortTrades(PreferenceAdapter.getTradeSortOrder(getContext())); } /** * Remove any showing dialogs, and show the requested one. * * @param id The ID of the dialog to show. * @param sideForDialog If this is for a specific card, this is the side of the trade the * card lives in. * @param positionForDialog If this is for a specific card, this is the position of the card in * the list. */ public void showDialog(final int id, final int sideForDialog, final int positionForDialog) 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()); if (id == TradeDialogFragment.DIALOG_SORT) { SortOrderDialogFragment newFragment = new SortOrderDialogFragment(); Bundle args = new Bundle(); args.putString(SortOrderDialogFragment.SAVED_SORT_ORDER, PreferenceAdapter.getTradeSortOrder(getContext())); newFragment.setArguments(args); newFragment.show(getFragmentManager(), FamiliarActivity.DIALOG_TAG); } else { /* Create and show the dialog. */ TradeDialogFragment newFragment = new TradeDialogFragment(); Bundle arguments = new Bundle(); arguments.putInt(FamiliarDialogFragment.ID_KEY, id); arguments.putInt(TradeDialogFragment.ID_SIDE, sideForDialog); arguments.putInt(TradeDialogFragment.ID_POSITION, positionForDialog); newFragment.setArguments(arguments); newFragment.show(getFragmentManager(), FamiliarActivity.DIALOG_TAG); } } /** * Save the current trade to the given filename. * * @param tradeName The name of the trade, to be used as a file name */ public void saveTrade(String tradeName) { FileOutputStream fos; /* Revert to added-order before saving */ sortTrades(SortOrderDialogFragment.KEY_ORDER + " " + SortOrderDialogFragment.SQL_ASC); try { /* MODE_PRIVATE will create the file (or replace a file of the same name) */ fos = this.getActivity().openFileOutput(tradeName, Context.MODE_PRIVATE); for (MtgCard cd : mListLeft) { fos.write(cd.toTradeString(LEFT).getBytes()); } for (MtgCard cd : mListRight) { fos.write(cd.toTradeString(RIGHT).getBytes()); } fos.close(); } catch (IOException e) { ToastWrapper.makeAndShowText(this.getActivity(), R.string.trader_toast_save_error, ToastWrapper.LENGTH_LONG); } catch (IllegalArgumentException e) { ToastWrapper.makeAndShowText(this.getActivity(), R.string.trader_toast_invalid_chars, ToastWrapper.LENGTH_LONG); } /* And resort to the expected order after saving */ sortTrades(PreferenceAdapter.getTradeSortOrder(getContext())); } /** * Load a a trade from the given filename. * * @param tradeName The name of the trade to load */ public void loadTrade(String tradeName) { BufferedReader br = null; try { /* Clear the current lists */ mListLeft.clear(); mListRight.clear(); /* Read each card, line by line, load prices along the way */ br = new BufferedReader(new InputStreamReader(this.getActivity().openFileInput(tradeName))); String line; while ((line = br.readLine()) != null) { try { MtgCard card = MtgCard.fromTradeString(line, getActivity()); card.setIndex(mOrderAddedIdx++); if (card.mSetName == null) { handleFamiliarDbException(false); return; } if (card.mSide == LEFT) { mListLeft.add(card); if (!card.mIsCustomPrice) { loadPrice(card); } } else if (card.mSide == RIGHT) { mListRight.add(card); if (!card.mIsCustomPrice) { loadPrice(card); } } } catch (NumberFormatException | IndexOutOfBoundsException e) { // This card line is junk, ignore it } } } catch (FileNotFoundException e) { /* Do nothing, the autosave doesn't exist */ } catch (IOException e) { ToastWrapper.makeAndShowText(this.getActivity(), e.getLocalizedMessage(), ToastWrapper.LENGTH_LONG); } finally { if (br != null) { IOUtils.closeQuietly(br); } } } /** * Handle an ActionBar item click. * * @param item the item clicked * @return true if the click was acted on */ @Override public boolean onOptionsItemSelected(MenuItem item) { /* Handle item selection */ switch (item.getItemId()) { case R.id.trader_menu_clear: { showDialog(TradeDialogFragment.DIALOG_CONFIRMATION, 0, 0); return true; } case R.id.trader_menu_settings: { showDialog(TradeDialogFragment.DIALOG_PRICE_SETTING, 0, 0); return true; } case R.id.trader_menu_save: { showDialog(TradeDialogFragment.DIALOG_SAVE_TRADE, 0, 0); return true; } case R.id.trader_menu_load: { showDialog(TradeDialogFragment.DIALOG_LOAD_TRADE, 0, 0); return true; } case R.id.trader_menu_delete: { showDialog(TradeDialogFragment.DIALOG_DELETE_TRADE, 0, 0); return true; } case R.id.trader_menu_sort: { /* show a dialog to change the sort criteria the list uses */ showDialog(TradeDialogFragment.DIALOG_SORT, 0, 0); return true; } case R.id.trader_menu_share: { shareTrade(); return true; } default: { return super.onOptionsItemSelected(item); } } } /** * Build a plaintext trade and share it. */ private void shareTrade() { StringBuilder sb = new StringBuilder(); /* Add all the cards to the StringBuilder from the left, tallying the price */ float totalPrice = 0; for (MtgCard card : mListLeft) { totalPrice += (card.toTradeShareString(sb, getString(R.string.wishlist_foil)) / 100.0f); } sb.append(String.format(Locale.US, PRICE_FORMAT + "%n", totalPrice)); /* Simple divider */ sb.append("--------\n"); /* Add all the cards to the StringBuilder from the right, tallying the price */ totalPrice = 0; for (MtgCard card : mListRight) { totalPrice += (card.toTradeShareString(sb, getString(R.string.wishlist_foil)) / 100.0f); } sb.append(String.format(Locale.US, PRICE_FORMAT, totalPrice)); /* Send the Intent on it's merry way */ Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, R.string.trade_share_title); sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString()); sendIntent.setType("text/plain"); try { startActivity(Intent.createChooser(sendIntent, getString(R.string.trader_share))); } catch (android.content.ActivityNotFoundException ex) { ToastWrapper.makeAndShowText(getActivity(), R.string.error_no_email_client, ToastWrapper.LENGTH_SHORT); } } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { super.onCreateOptionsMenu(menu, inflater); inflater.inflate(R.menu.trader_menu, menu); } /** * When the fragment resumes, get the price preference again and attempt to load the autosave * trade. */ @Override public void onResume() { super.onResume(); loadTrade(AUTOSAVE_NAME + TRADE_EXTENSION); } /** * When the fragment pauses, save the trade and cancel all pending price requests. */ @Override public void onPause() { super.onPause(); saveTrade(AUTOSAVE_NAME + TRADE_EXTENSION); } @Override protected void onCardPriceLookupFailure(MtgCard data, Throwable exception) { data.mMessage = exception.getLocalizedMessage(); data.mPriceInfo = null; } @Override protected void onCardPriceLookupSuccess(MtgCard data, MarketPriceInfo result) { updateTotalPrices(BOTH); try { sortTrades(PreferenceAdapter.getTradeSortOrder(getContext())); } catch (NullPointerException e) { /* couldn't get the preference, so don't bother sorting */ } } /** * This function iterates through the cards in the given list and sums together their prices. * * @param side RIGHT, LEFT, or BOTH, depending on the side to update */ public void updateTotalPrices(int side) { if (this.isAdded()) { if (side == LEFT || side == BOTH) { float totalPrice = 0; int totalCards = 0; boolean hasBadValues = false; /* Iterate through the list and either sum the price or mark it as "bad," (incomplete) */ for (MtgCard data : mListLeft) { totalCards += data.mNumberOf; if (data.hasPrice()) { totalPrice += data.mNumberOf * (data.mPrice / 100.0f); } else { hasBadValues = true; } } /* Set the color whether all values are loaded, and write the text */ int color = hasBadValues ? ContextCompat.getColor(getContext(), R.color.material_red_500) : ContextCompat.getColor(getContext(), getResourceIdFromAttr(R.attr.color_text)); final String leftPrice = String.format(Locale.US, PRICE_FORMAT, totalPrice) + " (" + totalCards + ")"; setTotalPrice(leftPrice, color, TradeFragment.LEFT); } if (side == RIGHT || side == BOTH) { float totalPrice = 0; int totalCards = 0; boolean hasBadValues = false; /* Iterate through the list and either sum the price or mark it as "bad," (incomplete) */ for (MtgCard data : mListRight) { totalCards += data.mNumberOf; if (data.hasPrice()) { totalPrice += data.mNumberOf * (data.mPrice / 100.0f); } else { hasBadValues = true; } } /* Set the color whether all values are loaded, and write the text */ int color = hasBadValues ? ContextCompat.getColor(getContext(), R.color.material_red_500) : ContextCompat.getColor(getContext(), getResourceIdFromAttr(R.attr.color_text)); final String rightPrice = String.format(Locale.US, PRICE_FORMAT, totalPrice) + " (" + totalCards + ")"; setTotalPrice(rightPrice, color, TradeFragment.RIGHT); } } } @Override public boolean shouldShowPrice() { return true; } @Override public MarketPriceInfo.PriceType getPriceSetting() { return PreferenceAdapter.getTradePrice(getContext()); } @Override public void setPriceSetting(MarketPriceInfo.PriceType priceSetting) { PreferenceAdapter.setTradePrice(getContext(), priceSetting); } /** * Called when the sorting dialog closes. Sort the trades with the new order. * * @param orderByStr The sort order string */ @Override public void receiveSortOrder(String orderByStr) { PreferenceAdapter.setTradeSortOrder(getContext(), orderByStr); sortTrades(orderByStr); } /** * Sort the trades. */ private void sortTrades(String sortOrder) { /* If no sort type specified, return */ TradeComparator tradeComparator = new TradeComparator(sortOrder); Collections.sort(mListLeft, tradeComparator); Collections.sort(mListRight, tradeComparator); getCardDataAdapter(LEFT).notifyDataSetChanged(); getCardDataAdapter(RIGHT).notifyDataSetChanged(); } private static class TradeComparator implements Comparator<MtgCard>, Serializable { final ArrayList<SortOrderDialogFragment.SortOption> options = new ArrayList<>(); /** * Constructor. It parses an "order by" string into search options. The first options have * higher priority. * * @param orderByStr The string to parse. It uses SQLite syntax: "KEY asc,KEY2 desc" etc */ TradeComparator(String orderByStr) { int idx = 0; for (String option : orderByStr.split(",")) { String key = option.split(" ")[0]; boolean ascending = option.split(" ")[1].equalsIgnoreCase(SortOrderDialogFragment.SQL_ASC); options.add(new SortOrderDialogFragment.SortOption(null, ascending, key, idx++)); } } /** * Compare two MtgCard objects based on all the search options in descending priority. * * @param card1 One card to compare * @param card2 The other card to compare * @return an integer < 0 if card1 is less than card2, 0 if they are equal, and > 0 if card1 * is greater than card2. */ @Override public int compare(MtgCard card1, MtgCard card2) { int retVal = 0; /* Iterate over all the sort options, starting with the high priority ones */ for (SortOrderDialogFragment.SortOption option : options) { try { /* Compare the entries based on the key */ switch (option.getKey()) { case CardDbAdapter.KEY_NAME: { retVal = card1.mName.compareTo(card2.mName); break; } case CardDbAdapter.KEY_COLOR: { retVal = card1.mColor.compareTo(card2.mColor); break; } case CardDbAdapter.KEY_SUPERTYPE: { retVal = card1.mType.compareTo(card2.mType); break; } case CardDbAdapter.KEY_CMC: { retVal = card1.mCmc - card2.mCmc; break; } case CardDbAdapter.KEY_POWER: { retVal = Float.compare(card1.mPower, card2.mPower); break; } case CardDbAdapter.KEY_TOUGHNESS: { retVal = Float.compare(card1.mToughness, card2.mToughness); break; } case CardDbAdapter.KEY_SET: { retVal = card1.mExpansion.compareTo(card2.mExpansion); break; } case SortOrderDialogFragment.KEY_PRICE: { retVal = Double.compare(card1.mPrice, card2.mPrice); break; } case SortOrderDialogFragment.KEY_ORDER: { retVal = Double.compare(card1.getIndex(), card2.getIndex()); break; } default: { break; } } } catch (NullPointerException e) { retVal = 0; } /* Adjust for ascending / descending */ if (!option.getAscending()) { retVal = -retVal; } /* If these two entries aren't equal, return. Otherwise continue and compare the * next value. */ if (retVal != 0) { return retVal; } } /* Guess they're totally equal */ return retVal; } } class TradeViewHolder extends CardDataViewHolder { private final TextView mCardSet; private final ImageView mCardFoil; private final TextView mCardPrice; private final int mSide; TradeViewHolder(ViewGroup view, int side) { super(view, R.layout.trader_row, TradeFragment.this.getCardDataAdapter(side), TradeFragment.this); mCardSet = itemView.findViewById(R.id.traderRowSet); mCardFoil = itemView.findViewById(R.id.traderRowFoil); mCardPrice = itemView.findViewById(R.id.traderRowPrice); itemView.setOnClickListener(this); itemView.setOnLongClickListener(this); mSide = side; } @Override public void onClickNotSelectMode(View view, int position) { showDialog(TradeDialogFragment.DIALOG_UPDATE_CARD, mSide, position); } } /** * Adapter to display the cards in each list. */ public class TradeDataAdapter extends CardDataAdapter<MtgCard, TradeViewHolder> { private final int side; TradeDataAdapter(ArrayList<MtgCard> values, int side) { super(values, TradeFragment.this); this.side = side; } @Override public TradeViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { return new TradeViewHolder(viewGroup, side); } @Override protected void onItemReadded() { TradeComparator tradeComparator = new TradeComparator( PreferenceAdapter.getTradeSortOrder(getContext())); switch (side) { case LEFT: { Collections.sort(mListLeft, tradeComparator); break; } case RIGHT: { Collections.sort(mListRight, tradeComparator); break; } } super.onItemReadded(); } @Override public void onBindViewHolder(TradeViewHolder holder, int position) { super.onBindViewHolder(holder, position); final MtgCard item = getItem(position); holder.itemView.findViewById(R.id.trade_row).setVisibility(View.VISIBLE); holder.setCardName(item.mName); holder.mCardSet.setText(item.mSetName); holder.mCardFoil.setVisibility(item.mIsFoil ? View.VISIBLE : View.GONE); if (item.hasPrice()) { holder.mCardPrice.setText(item.mNumberOf + "x " + item.getPriceString()); if (item.mIsCustomPrice) { holder.mCardPrice .setTextColor(ContextCompat.getColor(getContext(), R.color.material_green_500)); } else { holder.mCardPrice.setTextColor( ContextCompat.getColor(getContext(), getResourceIdFromAttr(R.attr.color_text))); } } else { holder.mCardPrice.setText(item.mNumberOf + "x " + item.mMessage); holder.mCardPrice.setTextColor(ContextCompat.getColor(getContext(), R.color.material_red_500)); } } } }