com.ovrhere.android.currencyconverter.ui.fragments.MainFragment.java Source code

Java tutorial

Introduction

Here is the source code for com.ovrhere.android.currencyconverter.ui.fragments.MainFragment.java

Source

/*
 * Copyright 2014 Jason J.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.ovrhere.android.currencyconverter.ui.fragments;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import android.annotation.SuppressLint;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Parcelable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.TextView;

import com.ovrhere.android.currencyconverter.R;
import com.ovrhere.android.currencyconverter.dao.CurrencyData;
import com.ovrhere.android.currencyconverter.model.CurrencyExchangeRateAsyncModel;
import com.ovrhere.android.currencyconverter.prefs.PreferenceUtils;
import com.ovrhere.android.currencyconverter.ui.adapters.CurrencyDataFilterListAdapter;
import com.ovrhere.android.currencyconverter.ui.adapters.CurrencyDataSpinnerAdapter;
import com.ovrhere.android.currencyconverter.utils.CompatClipboard;
import com.ovrhere.android.currencyconverter.utils.CurrencyCalculator;
import com.ovrhere.android.currencyconverter.utils.DateFormatter;
import com.ovrhere.android.currencyconverter.utils.KeyboardUtil;

/**
 * The main fragment where values are inputed and results shown.
 * @author Jason J.
 * @version 0.5.2-20141109
 */
public class MainFragment extends Fragment implements Handler.Callback, OnItemLongClickListener {
    /** The class name used for bundles. */
    final static private String CLASS_NAME = MainFragment.class.getSimpleName();
    /** The log tag for errors. */
    final static private String LOGTAG = CLASS_NAME;
    /** Whether or not to debug. */
    final static private boolean DEBUG = false;

    /** Bundle key: List<CurrencyData>/List<Parcellable>. The currently parsed list. */
    final static private String KEY_CURRENCY_LIST = CLASS_NAME + ".KEY_CURRENCY_LIST";

    /** Bundle key: String. The input to convert. */
    final static private String KEY_CURRENCY_VALUE_INPUT = CLASS_NAME + ".KEY_CURRENCY_VALUE_INPUT";
    /** Bundle key. Boolean. The value of {@link #currentlyUpdating} */
    final static private String KEY_CURRENTLY_UPDATING = CLASS_NAME + ".KEY_CURRENTLY_UPDATED";

    /////////////////////////////////////////////////////////////////////////////////////////////////
    /// End constants
    ////////////////////////////////////////////////////////////////////////////////////////////////
    /** The model for fetching currency info. */
    private CurrencyExchangeRateAsyncModel asyncModel = null;
    /** The handler for the model. */
    private Handler asyncHandler = new Handler(this);

    /** The currency list to use. Should be synchronized. */
    private List<CurrencyData> currencyList = new ArrayList<CurrencyData>();

    /** The from adapter. */
    private CurrencyDataSpinnerAdapter sourceCurrAdapter = null;
    /** The to adapter. */
    private CurrencyDataSpinnerAdapter destCurrAdapter = null;
    /** The output list adapter. */
    private CurrencyDataFilterListAdapter outputListAdapter = null;
    /** <code>true</code> if updating, <code>false</code> otherwise. */
    private boolean currentlyUpdating = false;

    /** The shared preference handle. */
    private SharedPreferences prefs = null;
    /** The state of the view. */
    private boolean viewBuilt = false;

    /////////////////////////////////////////////////////////////////////////////////////////////////
    /// Views start here
    ////////////////////////////////////////////////////////////////////////////////////////////////

    /** The spinner for the from Currency. */
    private Spinner sp_sourceCurr = null;
    /** The spinner for the to Currency. */
    private Spinner sp_destCurr = null;
    /** The current currency flag. */
    private ImageView img_currFlag = null;
    /** The current currency symbol. */
    private TextView tv_currSymbol = null;
    /** The current warning message. */
    private TextView tv_warning = null;
    /** The currency input. */
    private EditText et_currInput = null;
    /** The View for the spinny-progress bar for updates. */
    private View updateProgressSpin = null;

    /////////////////////////////////////////////////////////////////////////////////////////////////
    /// End members 
    ////////////////////////////////////////////////////////////////////////////////////////////////

    public MainFragment() {
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        synchronized (currencyList) {
            outState.putParcelableArrayList(KEY_CURRENCY_LIST, (ArrayList<? extends Parcelable>) currencyList);
        }
        if (viewBuilt) {
            outState.putString(KEY_CURRENCY_VALUE_INPUT, et_currInput.getText().toString());
            outState.putBoolean(KEY_CURRENTLY_UPDATING, currentlyUpdating);
        }
    }

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

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (PreferenceUtils.isFirstRun(getActivity())) {
            PreferenceUtils.setToDefault(getActivity());
        }
        prefs = PreferenceUtils.getPreferences(getActivity());
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_main, container, false);

        asyncModel = new CurrencyExchangeRateAsyncModel(getActivity());
        asyncModel.addMessageHandler(asyncHandler);
        processStateForRates(savedInstanceState);

        initAdapters();
        initInputViews(rootView);
        initOutputViews(rootView);
        initKeyboardHide(rootView);
        processSavedState(savedInstanceState);

        updateCurrencyAdapters();
        updateSourceCurrency();
        viewBuilt = true;
        return rootView;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        viewBuilt = false;
        asyncModel.dispose();
        asyncModel = null;
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        if (v.getId() == R.id.currConv_main_listView) {
            AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;

            CurrencyData currency = outputListAdapter.getItem(info.position);
            if (currency != null) {
                String title = getString(R.string.currConv_context_currencyAction, currency.getCurrencyCode());

                menu.setHeaderTitle(title);
                menu.add(Menu.CATEGORY_SECONDARY, 0, 0, android.R.string.copy);
                menu.add(Menu.CATEGORY_SECONDARY, 1, 1, R.string.currConv_context_detailedCopy);
            }
        }
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        if (item.getGroupId() == Menu.CATEGORY_SECONDARY) {
            AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
            switch (item.getItemId()) {
            case 0:
                int pos1 = info.position;
                copyConvertedValue(pos1, false);
                break;
            case 1:
                int pos2 = info.position;
                copyConvertedValue(pos2, true);
                break;
            }
        }
        return super.onContextItemSelected(item);
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////
    /// Initializer helper methods
    ////////////////////////////////////////////////////////////////////////////////////////////////

    /** Processes the save state for rates, requesting if there are none.
     * Requires asyncModel to be valid. */
    @SuppressWarnings("unchecked")
    public void processStateForRates(Bundle savedInstanceState) {
        if (savedInstanceState == null) {
            requestFreshExchangeRates(false);
            ;
        } else {
            currentlyUpdating = savedInstanceState.getBoolean(KEY_CURRENTLY_UPDATING);

            ArrayList<Parcelable> list = savedInstanceState.getParcelableArrayList(KEY_CURRENCY_LIST);
            synchronized (currencyList) {
                if (list != null) {
                    currencyList.clear();
                    try {
                        currencyList.addAll((Collection<? extends CurrencyData>) list);
                    } catch (ClassCastException e) {
                        Log.e(LOGTAG, "Current data invalid: " + e);
                    }
                }
                if (currencyList.isEmpty()) {
                    requestFreshExchangeRates(false);
                }
            }
        }
    }

    /** Processes the saved state, preferences and updates views accordingly.
     * Assumes all views to be valid.
     * @param savedInstanceState The current saved state.
     * @see #initInputViews(View)
     * @see #initOutputViews(View)     */
    private void processSavedState(Bundle savedInstanceState) {
        int sourceCurrSelect = prefs.getInt(getString(R.string.currConv_pref_KEY_SOURCE_CURRENCY_INDEX), 0);
        int destCurrSelect = prefs.getInt(getString(R.string.currConv_pref_KEY_DEST_CURRENCY_INDEX), 0);

        sp_sourceCurr.setSelection(sourceCurrSelect);
        sp_destCurr.setSelection(destCurrSelect);

        String input = "";
        if (savedInstanceState != null) {
            input = savedInstanceState.getString(KEY_CURRENCY_VALUE_INPUT) == null ? input
                    : savedInstanceState.getString(KEY_CURRENCY_VALUE_INPUT);

            et_currInput.setText(input);
        }
        CurrencyData cdata = (CurrencyData) sp_sourceCurr.getSelectedItem();
        if (cdata != null) {
            outputListAdapter.updateCurrentValue(cdata, convertToDouble(input));
        }
    }

    /** Initializes the output views such as textviews, images & listview.
     * Assumes adapters are valid.
     * @param rootView The rootview to configure   
     * @see #initAdapters()  */
    private void initOutputViews(View rootView) {
        ListView outputListView = (ListView) rootView.findViewById(R.id.currConv_main_listView);
        outputListView.setAdapter(outputListAdapter);
        outputListView.setOnItemLongClickListener(this);
        registerForContextMenu(outputListView);

        updateProgressSpin = rootView.findViewById(R.id.currConv_main_progressSpin);
        checkProgressBar();

        tv_currSymbol = (TextView) rootView.findViewById(R.id.currConv_main_text_currSymbol);
        tv_warning = (TextView) rootView.findViewById(R.id.currConv_main_text_warning);

        img_currFlag = (ImageView) rootView.findViewById(R.id.currConv_main_image_currFlag);
        //show or hide flags, depending on bool.
        boolean showFlags = getResources().getBoolean(R.bool.currconv_showflags);
        img_currFlag.setVisibility(showFlags ? View.VISIBLE : View.GONE);
    }

    /** Initializes all input views. Assumes adapters are valid.
     * @param rootView The rootview to configure   
     * @see #initAdapters()    */
    private void initInputViews(View rootView) {
        sp_sourceCurr = (Spinner) rootView.findViewById(R.id.currConv_main_spinner_currencySource);
        sp_sourceCurr.setAdapter(sourceCurrAdapter);
        sp_sourceCurr.setOnItemSelectedListener(sourceItemSelectedListener);

        sp_destCurr = (Spinner) rootView.findViewById(R.id.currConv_main_spinner_currencyDest);
        sp_destCurr.setAdapter(destCurrAdapter);
        sp_destCurr.setOnItemSelectedListener(destItemSelectListener);

        et_currInput = (EditText) rootView.findViewById(R.id.currConv_main_edittext_valueToConv);
        et_currInput.addTextChangedListener(valueInputListener);
    }

    /** Initializes adapters for output list and spinners. */
    private void initAdapters() {
        outputListAdapter = new CurrencyDataFilterListAdapter(getActivity());

        sourceCurrAdapter = new CurrencyDataSpinnerAdapter(getActivity(), android.R.layout.simple_list_item_1);
        destCurrAdapter = new CurrencyDataSpinnerAdapter(getActivity(), android.R.layout.simple_list_item_1, true);
        destCurrAdapter.setSelectAllText(getString(R.string.currConv_spinner_dest_selectAll));
    }

    /** Initializes the hiding of the keyboard for all non-edit-texts views.
     * @param rootView The rootview to attach to.
     */
    private void initKeyboardHide(View rootView) {
        final FragmentActivity activity = getActivity();
        rootView.setOnTouchListener(new View.OnTouchListener() {
            //this is will bubble until consumed (such as EditText)
            @SuppressLint("ClickableViewAccessibility")
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                KeyboardUtil.hideSoftKeyboard(activity);
                return false; //we aren't consume either
            }
        });
    }

    /** Updates all currency adapters the the current value of #currencyList 
     * Assumes all adapters are set. */
    private void updateCurrencyAdapters() {
        synchronized (currencyList) {
            sourceCurrAdapter.setCurrencyData(currencyList);
            destCurrAdapter.setCurrencyData(currencyList);
            outputListAdapter.setCurrencyData(currencyList);
        }
    }

    /** Updates source views to match source currency. */
    private void updateSourceCurrency() {
        if (sp_sourceCurr == null || tv_warning == null || tv_currSymbol == null || img_currFlag == null
                || getActivity() == null) {
            return; //nothing can be set.
        }
        int position = sp_sourceCurr.getSelectedItemPosition();

        if (position < 0 || sourceCurrAdapter.getCount() < 1) {
            return; //nothing to show yet.
        }
        CurrencyData data = null;
        try {
            data = sourceCurrAdapter.getItem(position);
        } catch (IndexOutOfBoundsException e) {
            Log.w(LOGTAG, "Index out:" + position);
        }

        if (data == null) {
            Log.w(LOGTAG, "Irregular behaviour skipping update.");
            return;
        }
        Resources r = getActivity().getResources();
        checkTimestampWarning(data);

        tv_currSymbol.setText(data.getCurrencySymbol());
        int flagId = data.getFlagResource();
        if (flagId >= 0) {
            img_currFlag.setImageDrawable(r.getDrawable(flagId));
        }
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////
    /// Helper Methods
    ////////////////////////////////////////////////////////////////////////////////////////////////

    /** Copies the converted currency at the given position. 
     * @param position The position the item was selected from.
     * @param detailed <code>true</code> to copy with multiple decimals places,
     * <code>false</code> for the basic currency decimals.    */
    private void copyConvertedValue(int position, boolean detailed) {
        CurrencyData sourceCurrency = (CurrencyData) sp_sourceCurr.getSelectedItem();
        CurrencyData destCurrency = outputListAdapter.getItem(position);

        if (destCurrency != null && sourceCurrency != null) {
            double amount = convertToDouble(et_currInput.getText().toString());
            //produces: $ [converted value] CODE
            String value = destCurrency.getCurrencySymbol()
                    + CurrencyCalculator.format(destCurrency,
                            CurrencyCalculator.convert(sourceCurrency, destCurrency, amount), detailed)
                    + " " + destCurrency.getCurrencyCode();

            String label = getString(R.string.currConv_clipboard_label_copiedCurrency);
            CompatClipboard.copyToClipboard(getActivity(), label, value);
        }
    }

    /** Hides/Shows the progress bar based on the value of {@link #currentlyUpdating}. */
    private void checkProgressBar() {
        updateProgressSpin.setVisibility(currentlyUpdating ? View.VISIBLE : View.GONE);
    }

    /** Takes the currency time stamp and checks if request for a new update.
     * and update views. 
     * @param currencyData The current data to parse. 
     * @return The readable timestamp.    */
    private void checkTimestampToUpdate(CurrencyData currencyData) {
        long updateInterval = prefs.getInt(getString(R.string.currConv_pref_KEY_UPDATE_CURRENCY_INTERVAL), 0);
        long interval = new Date().getTime() - currencyData.getModifiedDate().getTime();
        if (updateInterval < interval) {
            requestFreshExchangeRates(true);
            checkTimestampWarning(currencyData);
        }
    }

    /** Takes the currency time stamp and checks if to display message.
     * @param currencyData The current data to parse. 
     * @return The readable timestamp.    */
    private void checkTimestampWarning(CurrencyData currencyData) {
        long updateInterval = prefs.getInt(getString(R.string.currConv_pref_KEY_UPDATE_CURRENCY_INTERVAL), 0);
        long interval = new Date().getTime() - currencyData.getModifiedDate().getTime();

        if (updateInterval < interval) {
            String timestamp = DateFormatter.dateToRelativeDate(getActivity(), currencyData.getModifiedDate());
            tv_warning.setText(getString(R.string.currConv_cachedRate_warning, timestamp));
            tv_warning.setVisibility(View.VISIBLE);
            requestFreshExchangeRates(true);
        } else {
            tv_warning.setVisibility(View.GONE);
        }
    }

    /** Requests a fresh list of exchange rates from the model. 
     * @param forceUpdate <code>true</code> to force online update, 
     * <code>false</code> to forego it. */
    private void requestFreshExchangeRates(boolean forceUpdate) {
        asyncModel.sendMessage(CurrencyExchangeRateAsyncModel.REQUEST_GET_ALL_RECORDS, forceUpdate);
    }

    /** Parses input and sends it to adapter for calculation(s).
     * @param input The input to strip & parse.       */
    private void calculateOutput(String input) {
        double value = convertToDouble(input);
        outputListAdapter.updateCurrentValue(value);
    }

    /** Sets an integer preference 
     * @param stringRes The preference string resource
     * @param value The value to insert.    */
    private void putIntPref(int stringRes, int value) {
        SharedPreferences.Editor editor = prefs.edit();
        editor.putInt(getString(stringRes), value);
        editor.commit();
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////
    /// Utility function
    ////////////////////////////////////////////////////////////////////////////////////////////////

    /** Converts input to pure double. Only 0-9 and "." allowed
     * @param input The string input. If empty, returns 0.
     * @return The parsed double.     */
    static private double convertToDouble(String input) {
        if (input.isEmpty()) {
            return 0.0d;
        }
        //scrub input from non-valid input
        return Double.parseDouble(input.replaceAll("[^\\.0-9]", ""));
    }

    /////////////////////////////////////////////////////////////////////////////////////////////////
    /// Implemented interfaces
    ////////////////////////////////////////////////////////////////////////////////////////////////
    /** The input listener for when entering values. */
    private TextWatcher valueInputListener = new TextWatcher() {
        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            //nothing to do
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            //nothing to do
        }

        @Override
        public void afterTextChanged(Editable s) {
            calculateOutput(s.toString().trim());
        }
    };

    /** The selection listener for the "from" spinner. */
    private OnItemSelectedListener sourceItemSelectedListener = new OnItemSelectedListener() {
        @Override
        public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
            updateSourceCurrency();
            CurrencyData currency = sourceCurrAdapter.getItem(position);
            if (currency != null) {
                outputListAdapter.updateCurrentValue(currency, 0);
                calculateOutput(et_currInput.getText().toString());
            }
            putIntPref(R.string.currConv_pref_KEY_SOURCE_CURRENCY_INDEX, position);
        };

        @Override
        public void onNothingSelected(android.widget.AdapterView<?> parent) {
            //do nothing for now
        };
    };
    /** The selection listener for the "to" spinner. */
    private OnItemSelectedListener destItemSelectListener = new OnItemSelectedListener() {
        @Override
        public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
            CurrencyData data = destCurrAdapter.getItem(position);
            if (data == null) {
                outputListAdapter.setContraints(new String[] {});
            } else {
                outputListAdapter.setContraints(new String[] { data.getCurrencyCode() });
            }
            putIntPref(R.string.currConv_pref_KEY_DEST_CURRENCY_INDEX, position);
        };

        @Override
        public void onNothingSelected(android.widget.AdapterView<?> parent) {
            //do nothing for now
        }
    };

    @Override
    public boolean onItemLongClick(android.widget.AdapterView<?> parent, View view, int position, long id) {
        CurrencyData selectedCurrency = outputListAdapter.getItem(position);
        if (selectedCurrency != null) {
            Log.d(LOGTAG, "Testing value: " + selectedCurrency.getCurrencyCode());
        }
        return false;
    };

    @Override
    public boolean handleMessage(Message msg) {
        if (DEBUG) {
            Log.d(LOGTAG, "Message received: " + msg.what);
        }
        switch (msg.what) {
        case CurrencyExchangeRateAsyncModel.REPLY_RECORDS_RESULT:
            try {
                @SuppressWarnings("unchecked")
                List<CurrencyData> list = (List<CurrencyData>) msg.obj;
                if (!list.isEmpty()) {
                    @SuppressWarnings("unused")
                    //cast type checking.
                    CurrencyData data = (CurrencyData) list.get(0);
                }
                synchronized (currencyList) {
                    currencyList.clear();
                    currencyList.addAll(list);
                }
                updateCurrencyAdapters();
                updateSourceCurrency();
                if (!currentlyUpdating) {
                    checkTimestampToUpdate(currencyList.get(0));
                } else {
                    currentlyUpdating = false;
                    checkProgressBar();
                }
            } catch (ClassCastException e) {
                Log.e(LOGTAG, "Current data invalid: " + e);
            }
            return true;

        case CurrencyExchangeRateAsyncModel.NOTIFY_INITIALIZING_DATABASE:
            return true;
        case CurrencyExchangeRateAsyncModel.NOTIFY_UPDATING_RATES:
            currentlyUpdating = true;
            checkProgressBar();
            return true;
        case CurrencyExchangeRateAsyncModel.ERROR_REQUEST_FAILED:
        case CurrencyExchangeRateAsyncModel.ERROR_REQUEST_TIMEOUT:
            currentlyUpdating = false;
            checkProgressBar();
            return true;
        }

        return false;
    }

}