Java tutorial
/* * 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.model; import java.io.IOException; import java.io.InputStream; import java.net.SocketTimeoutException; import java.sql.SQLException; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.List; import org.xmlpull.v1.XmlPullParserException; import android.content.Context; import android.content.SharedPreferences; import android.content.res.Resources; import android.support.v4.app.FragmentActivity; import android.support.v4.app.FragmentManager; import android.util.Log; import com.ovrhere.android.currencyconverter.R; import com.ovrhere.android.currencyconverter.dao.CurrencyData; import com.ovrhere.android.currencyconverter.dao.SimpleExchangeRates; import com.ovrhere.android.currencyconverter.model.asyncmodel.AsyncModel; import com.ovrhere.android.currencyconverter.model.asyncmodel.RunnableHeadlessFragment; import com.ovrhere.android.currencyconverter.model.currencyrequest.YahooApiCurrencyJsonParser; import com.ovrhere.android.currencyconverter.model.currencyrequest.YahooApiCurrencyRequest; import com.ovrhere.android.currencyconverter.model.currencyrequest.YahooApiCurrencyXmlParser; import com.ovrhere.android.currencyconverter.prefs.PreferenceUtils; import com.ovrhere.android.currencyconverter.utils.Timestamp; /** * * Remember to call {@link #dispose()} to start cleanup and release * the {@link Context}. * * @author Jason J. * @version 0.4.1-20140929 */ public class CurrencyExchangeRateAsyncModel extends AsyncModel implements YahooApiCurrencyRequest.OnRequestEventListener { /** Class name for debugging purposes. */ final static private String CLASS_NAME = CurrencyExchangeRateAsyncModel.class.getSimpleName(); /** The log tag to use. */ final static private String LOGTAG = CLASS_NAME; /** The boolean for debugging. */ final static private boolean DEBUG = false; /** The tag for this fragment. */ final static private String RUNNABLE_HEADLESS_FRAG_TAG = CLASS_NAME + "." + RunnableHeadlessFragment.class.getSimpleName(); ///////////////////////////////////////////////////////////////////////////////////////////////// /// End private constants //////////////////////////////////////////////////////////////////////////////////////////////// /** Retrieves all records from the database. * If accompanied by a boolean <code>true</code>, * cached records are forced to update and ONLY replies if they are * updated. * Responds via {@link #REPLY_RECORDS_RESULT}. */ final static public int REQUEST_GET_ALL_RECORDS = 0x005; /** Reply of relevant records. * Accompanied by a {@link List} of {@link CurrencyData}. */ final static public int REPLY_RECORDS_RESULT = 0x100; /** Notification for when records are being initialized. */ final static public int NOTIFY_INITIALIZING_DATABASE = 0x200; /** Notification for when records are being updated from web.*/ final static public int NOTIFY_UPDATING_RATES = 0x201; /** Error given if request timesout. */ final static public int ERROR_REQUEST_TIMEOUT = 0x4001; ///////////////////////////////////////////////////////////////////////////////////////////////// /// End Request/Reply constants //////////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////////// /// End bundle key constants //////////////////////////////////////////////////////////////////////////////////////////////// /** The local model to access the database. Uses context. */ private CurrencyExchangeRateModel mLocalModel = null; /** Resources handler. Uses context. */ private Resources res = null; /** The fragment manager for getting the fragment. */ private FragmentManager mfragManager = null; /** The local access to shared preferences. */ private SharedPreferences prefs = null; /** A list of all currencies. */ final private List<String> currencyCodeList = new ArrayList<String>(); /** Whether or not the current request is using JSON. * Set in {@link #requestApiValues(List)} */ private boolean usingJson = false; /** Whether the model is disposed. false by default, true in #dispose() */ volatile private boolean isDisposed = false; /** Whether or not the model is updating. * Set true in {@link #onStart(InputStream)} and false in {@link #onComplete()} * or {@link #onException(Exception)} */ volatile private boolean isUpdating = false; /** Runnable to dispose of the contexts. This is so if we are disposing * while a thread is still running we can dispose AFTER we do the work. */ private Runnable asyncDispose = new Runnable() { @Override public void run() { mLocalModel = null; res = null; mfragManager = null; prefs = null; //clear all contexts if (DEBUG) { Log.d(LOGTAG, "asyncDispose in: " + CurrencyExchangeRateAsyncModel.this.hashCode()); } } }; ///////////////////////////////////////////////////////////////////////////////////////////////// /// End class members //////////////////////////////////////////////////////////////////////////////////////////////// /** Initializes asynchronous model. * @param activity The application's activity. */ public CurrencyExchangeRateAsyncModel(FragmentActivity activity) { this.mLocalModel = new CurrencyExchangeRateModel(activity); this.res = activity.getResources(); this.currencyCodeList.addAll(Arrays.asList(res.getStringArray(R.array.currConv_rateOrder))); mfragManager = activity.getSupportFragmentManager(); //prepare the headless fragment in advance. RunnableHeadlessFragment runFrag = getHeadlessFrag(); if (runFrag.getRunnable() != null) { //if there are any runnables try { YahooApiCurrencyRequest req = (YahooApiCurrencyRequest) runFrag.getRunnable(); //update any running to the current object req.setOnRequestEventListener(this); } catch (ClassCastException e) { } } this.prefs = PreferenceUtils.getPreferences(activity); } @Override public void dispose() { super.dispose(); isDisposed = true; if (!isUpdating) { disposeHelper(); } } @Override public int sendMessage(int what, Object object) { if (mLocalModel == null) { return 1; } if (DEBUG) { Log.d(LOGTAG, "sendMessage: " + what); } switch (what) { case REQUEST_GET_ALL_RECORDS: if (((Boolean) object)) { requestRecords(true); } else { requestRecords(); } return 0; default: //break; } return -1; } ///////////////////////////////////////////////////////////////////////////////////////////////// /// Request methods //////////////////////////////////////////////////////////////////////////////////////////////// /** Convenience for {@link #requestRecords(boolean)} with <code>false</code>. */ private void requestRecords() { requestRecords(false); } /* * Response logic is as follows: * -If records are empty/forced update: request update * -A) If continuing as normal: * + update database * + re-fetch db values and send values * -B) If the model is "disposed" during an update: * + Set a check for disposal * + Run request until update and update the model-x * + dispose model-x after update; releasing context * + if request is made in new model-y while model-x is still running: * re-attach model to runnable, and follow route "A" above */ /** The basic request of records. * @param force <code>true</code> forces and update, <code>false</code> * does not.*/ private void requestRecords(boolean force) { List<CurrencyData> records = mLocalModel.getAllRecords(); if (force) { requestApiValues(records); return; } if (records.isEmpty()) { if (initDefaultDatabase()) { records = mLocalModel.getAllRecords(); requestApiValues(records); } else { notifyHandlers(ERROR_REQUEST_FAILED, null); return; } } replyWithRecords(records); } /** The request for api values from {@link YahooApiCurrencyRequest}. */ private void requestApiValues(List<CurrencyData> records) { if (isUpdating) { return; //we are already updating, it will return eventually } isUpdating = true; //the lazy approach usingJson = prefs.getBoolean(res.getString(R.string.currConv_pref_KEY_USE_JSON_REQUEST), false); if (records.isEmpty()) { if (DEBUG) { Log.w(LOGTAG, "The records are empty. What are we to update?"); } return; //we will not try } List<String> destCodes = new ArrayList<String>(); for (CurrencyData cd : records) { destCodes.add(cd.getCurrencyCode()); } YahooApiCurrencyRequest request = new YahooApiCurrencyRequest(currencyCodeList, currencyCodeList, this); request.setJsonFormat(usingJson); if (DEBUG) { Log.d(LOGTAG, "Using request: " + request.toString()); } notifyHandlers(NOTIFY_UPDATING_RATES, null); RunnableHeadlessFragment runFrag = getHeadlessFrag(); runFrag.setRunable(request); runFrag.startThread(); } ///////////////////////////////////////////////////////////////////////////////////////////////// /// Helper methods //////////////////////////////////////////////////////////////////////////////////////////////// /** Sends reply with records, ensuring the drawables are placed first. * @param records The records to attach drawables to and send. */ private void replyWithRecords(List<CurrencyData> records) { for (Iterator<CurrencyData> iterator = records.iterator(); iterator.hasNext();) { FlagResourceMap.setCurrencyFlagDrawable(res, iterator.next()); } notifyHandlers(REPLY_RECORDS_RESULT, records); } /** Sets up the local database for the first time. * @return <code>true</code> if successful. */ private boolean initDefaultDatabase() { try { notifyHandlers(NOTIFY_INITIALIZING_DATABASE, null); mLocalModel.insertRecord(createCurrencyData(R.string.currConv_USD_symbol, R.string.currConv_USD_code, R.string.currConv_USD_name, R.array.currConv_USD_defRates)); mLocalModel.insertRecord(createCurrencyData(R.string.currConv_CAD_symbol, R.string.currConv_CAD_code, R.string.currConv_CAD_name, R.array.currConv_CAD_defRates)); mLocalModel.insertRecord(createCurrencyData(R.string.currConv_EUR_symbol, R.string.currConv_EUR_code, R.string.currConv_EUR_name, R.array.currConv_EUR_defRates)); mLocalModel.insertRecord(createCurrencyData(R.string.currConv_GBP_symbol, R.string.currConv_GBP_code, R.string.currConv_GBP_name, R.array.currConv_GBP_defRates)); mLocalModel.insertRecord(createCurrencyData(R.string.currConv_JPY_symbol, R.string.currConv_JPY_code, R.string.currConv_JPY_name, R.array.currConv_JPY_defRates)); } catch (ParseException e) { Log.e(LOGTAG, "Float parse error occurred"); e.printStackTrace(); return false; } catch (SQLException e) { Log.e(LOGTAG, "Database error occurred"); e.printStackTrace(); return false; } return true; } /** * Creates {@link CurrencyData} based upon the ids supplied. * @param symbolId The symbol string resource to use (e.g. $) * @param codeId The 3 letter code string resource to use * @param nameId The name string resource to use * @param rateArrayId The rate string array resource to use. * @return CurrencyData * @throws ParseException * @throws {@link NumberFormatException} */ private CurrencyData createCurrencyData(int symbolId, int codeId, int nameId, int rateArrayId) throws ParseException { String defRates[] = res.getStringArray(rateArrayId); if (defRates.length != currencyCodeList.size()) { if (DEBUG) { Log.w(LOGTAG, "Irregular behavior: Mismatched lists?"); } throw new IndexOutOfBoundsException(); } HashMap<String, Double> exchangeRates = new HashMap<String, Double>(); final int SIZE = currencyCodeList.size(); for (int index = 0; index < SIZE; index++) { exchangeRates.put(currencyCodeList.get(index), Double.parseDouble(defRates[index])); } CurrencyData.Builder build = new CurrencyData.Builder(); build.setCurrency(res.getString(symbolId), res.getString(codeId), res.getString(nameId)) .setModifiedTimestamp(res.getString(R.string.currConv_defRate_updateTime)) .setExchangeRates(exchangeRates); return build.create(); } /** Updates the local model's record rates * @param rates The rates to apply to the records. */ private void updateRecordRates(SimpleExchangeRates rates) { List<CurrencyData> records = mLocalModel.getAllRecords(); String timestamp = Timestamp.getUtc(); for (CurrencyData record : records) { String sCode = record.getCurrencyCode(); HashMap<String, Double> rateMap = new HashMap<String, Double>(); for (String dCode : currencyCodeList) { double rate = rates.getRate(sCode, dCode); rateMap.put(dCode, rate); } rateMap.put(sCode, 1.0d); //put itself, for safety. int id = record.getId(); CurrencyData.Builder builder = new CurrencyData.Builder(); try { builder.setId(id).setCurrency(record.getCurrencySymbol(), sCode, record.getCurrencyName()) .setModifiedTimestamp(timestamp).setExchangeRates(rateMap); if (DEBUG) { Log.d(LOGTAG, "Updating: " + id + " - " + builder.create()); } mLocalModel.modifyRecord(id, builder.create()); } catch (SQLException e) { if (DEBUG) { Log.e(LOGTAG, "Record " + record.getId() + " not updated: " + e); } } catch (ParseException e) { if (DEBUG) { e.printStackTrace(); } } catch (IllegalArgumentException e) { if (DEBUG) { Log.e(LOGTAG, "Bad argument given at: " + sCode + " - " + e); e.printStackTrace(); throw e; //this should kill } //TODO thought: perhaps this should wipe database/reset it? } catch (Exception e) { if (DEBUG) { Log.e(LOGTAG, "Other exception occurred at: " + sCode + " - " + e); e.printStackTrace(); } } } } /** Gets headless fragment if found in manager. Otherwise, places it into * the manager * @return The headless fragment to attach runnables to. */ private RunnableHeadlessFragment getHeadlessFrag() { RunnableHeadlessFragment runFrag = (RunnableHeadlessFragment) mfragManager .findFragmentByTag(RUNNABLE_HEADLESS_FRAG_TAG); if (runFrag == null) { runFrag = new RunnableHeadlessFragment(); mfragManager.beginTransaction().add(runFrag, RUNNABLE_HEADLESS_FRAG_TAG).commit(); } return runFrag; } /** Disposes via the runnable method. */ private void disposeHelper() { if (isDisposed) { asyncDispose.run(); } } ///////////////////////////////////////////////////////////////////////////////////////////////// /// Implemented Listeners //////////////////////////////////////////////////////////////////////////////////////////////// @Override public void onStart(InputStream in) { if (DEBUG) { Log.d(LOGTAG, "Starting in object: " + this.hashCode()); } isUpdating = true; SimpleExchangeRates rates = null; try { if (usingJson) { YahooApiCurrencyJsonParser parser = new YahooApiCurrencyJsonParser(); rates = parser.parseJsonStream(in); } else { YahooApiCurrencyXmlParser parser = new YahooApiCurrencyXmlParser(); rates = parser.parseXmlStream(in); } in.close(); } catch (XmlPullParserException e) { if (DEBUG) { e.printStackTrace(); } } catch (IOException e) { if (DEBUG) { e.printStackTrace(); } } updateRecordRates(rates); disposeHelper(); } @Override public void onException(Exception exception) { if (DEBUG) { Log.d(LOGTAG, "Exception in object: " + this.hashCode()); } try { throw exception; } catch (SocketTimeoutException e) { if (DEBUG) { e.printStackTrace(); } notifyHandlers(ERROR_REQUEST_TIMEOUT, null); } catch (IOException e) { if (DEBUG) { e.printStackTrace(); } notifyHandlers(ERROR_REQUEST_FAILED, null); } catch (Exception e) { if (DEBUG) { e.printStackTrace(); } notifyHandlers(ERROR_REQUEST_FAILED, null); } isUpdating = false; disposeHelper(); } @Override public void onResponseCode(int responseCode) { if (DEBUG) { Log.d(LOGTAG, "Response code: " + responseCode); } } @Override public void onComplete() { if (DEBUG) { Log.d(LOGTAG, "request: onComplete in: " + this.hashCode()); } //reply when finished. replyWithRecords(mLocalModel.getAllRecords()); getHeadlessFrag().setRunable(null); //remove runnable isUpdating = false; disposeHelper(); } }