co.carlosjimenez.android.currencyalerts.app.sync.ForexSyncAdapter.java Source code

Java tutorial

Introduction

Here is the source code for co.carlosjimenez.android.currencyalerts.app.sync.ForexSyncAdapter.java

Source

/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2016 Carlos Andres Jimenez <apps@carlosandresjimenez.co>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package co.carlosjimenez.android.currencyalerts.app.sync;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SyncRequest;
import android.content.SyncResult;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.IntDef;
import android.support.v4.content.LocalBroadcastManager;
import android.text.format.Time;
import android.util.Log;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Vector;

import co.carlosjimenez.android.currencyalerts.app.R;
import co.carlosjimenez.android.currencyalerts.app.Utility;
import co.carlosjimenez.android.currencyalerts.app.data.Alert;
import co.carlosjimenez.android.currencyalerts.app.data.ForexContract;

public class ForexSyncAdapter extends AbstractThreadedSyncAdapter {

    public static final String LOG_TAG = ForexSyncAdapter.class.getSimpleName();

    public static final String ACTION_DATA_UPDATED = "co.carlosjimenez.android.currencyalerts.app.ACTION_DATA_UPDATED";
    public static final String FOREX_DATA_STATUS = "FOREX_DATA_STATUS";

    public static final int FOREX_DAYS_TO_KEEP = 40;

    public static final int FOREX_STATUS_OK = 0;
    public static final int FOREX_STATUS_SERVER_DOWN = 1;
    public static final int FOREX_STATUS_SERVER_INVALID = 2;

    public static final int FOREX_STATUS_UNKNOWN = 3;
    public static final int FOREX_STATUS_INVALID = 4;

    private Alert mAlertData;
    private double mCurrentAlertRate;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({ FOREX_STATUS_OK, FOREX_STATUS_SERVER_DOWN, FOREX_STATUS_SERVER_INVALID, FOREX_STATUS_UNKNOWN,
            FOREX_STATUS_INVALID })
    public @interface ForexStatus {
    }

    public ForexSyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
    }

    /**
     * Helper method to schedule the sync adapter periodic execution
     */
    public static void configurePeriodicSync(Context context, int syncInterval, int flexTime) {
        Account account = getSyncAccount(context);
        String authority = context.getString(R.string.content_authority);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            // we can enable inexact timers in our periodic sync
            SyncRequest request = new SyncRequest.Builder().syncPeriodic(syncInterval, flexTime)
                    .setSyncAdapter(account, authority).setExtras(new Bundle()).build();
            ContentResolver.requestSync(request);
        } else {
            ContentResolver.addPeriodicSync(account, authority, new Bundle(), syncInterval);
        }
    }

    /**
     * Helper method to have the sync adapter sync immediately
     *
     * @param context The context used to access the account service
     */
    public static void syncImmediately(Context context) {
        Bundle bundle = new Bundle();
        bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
        bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
        ContentResolver.requestSync(getSyncAccount(context), context.getString(R.string.content_authority), bundle);
    }

    /**
     * Helper method to get the fake account to be used with SyncAdapter, or make a new one
     * if the fake account doesn't exist yet.  If we make a new account, we call the
     * onAccountCreated method so we can initialize things.
     *
     * @param context The context used to access the account service
     * @return a fake account.
     */
    public static Account getSyncAccount(Context context) {
        // Get an instance of the Android account manager
        AccountManager accountManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);

        // Create the account type and default account
        Account newAccount = new Account(context.getString(R.string.app_name),
                context.getString(R.string.sync_account_type));

        // If the password doesn't exist, the account doesn't exist
        if (null == accountManager.getPassword(newAccount)) {

            /*
             * Add the account and account type, no password or user data
             * If successful, return the Account object, otherwise report an error.
             */
            if (!accountManager.addAccountExplicitly(newAccount, "", null)) {
                return null;
            }
            /*
             * If you don't set android:syncable="true" in
             * in your <provider> element in the manifest,
             * then call ContentResolver.setIsSyncable(account, AUTHORITY, 1)
             * here.
             */

            onAccountCreated(newAccount, context);
        }
        return newAccount;
    }

    private static void onAccountCreated(Account newAccount, Context context) {
        // Interval at which to sync with the rates, in seconds.
        // 60 seconds (1 minute) * 180 = 3 hours
        int syncFrequency = Utility.getSyncFrequency(context);

        if (syncFrequency != -1) {
            int syncInterval = 60 * syncFrequency;
            int syncFlextime = syncInterval / 3;

            ForexSyncAdapter.configurePeriodicSync(context, syncInterval, syncFlextime);
        }

        /*
         * Without calling setSyncAutomatically, our periodic sync will not be enabled.
         */
        ContentResolver.setSyncAutomatically(newAccount, context.getString(R.string.content_authority), true);

        /*
         * Finally, let's do a sync to get things started
         */
        syncImmediately(context);
    }

    public static void initializeSyncAdapter(Context context) {
        getSyncAccount(context);
    }

    /**
     * Sets the forex status into shared preference.  This function should not be called from
     * the UI thread because it uses commit to write to the shared preferences.
     *
     * @param c           Context to get the PreferenceManager from.
     * @param forexStatus The IntDef value to set
     */
    static private void setForexStatus(Context c, @ForexStatus int forexStatus) {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(c);
        SharedPreferences.Editor spe = sp.edit();
        spe.putInt(c.getString(R.string.pref_forex_status_key), forexStatus);
        spe.commit();
    }

    /**
     * Sets the forex sync date into shared preference.  This function should not be called from
     * the UI thread because it uses commit to write to the shared preferences.
     *
     * @param c             Context to get the PreferenceManager from.
     * @param forexSyncDate The IntDef value to set
     */
    static private void setForexSyncDate(Context c, long forexSyncDate) {
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(c);
        SharedPreferences.Editor spe = sp.edit();
        spe.putLong(c.getString(R.string.pref_forex_sync_date_key), forexSyncDate);
        spe.commit();
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider,
            SyncResult syncResult) {
        // These two need to be declared outside the try/catch
        // so that they can be closed in the finally block.
        HttpURLConnection urlConnection = null;
        BufferedReader reader = null;
        String currencies = "";

        // Will contain the raw JSON response as a string.
        String forexJsonStr = null;

        try {
            mAlertData = Utility.getAlertSettings(getContext(), false);

            // Construct the URL for the OpenWeatherMap query
            // Possible parameters are available at currencyconverterapi API page.
            final String FOREX_BASE_URL = "http://free.currencyconverterapi.com/api/v3/convert?";
            final String QUERY_PARAM = "q";
            String currencyQueryStr = Utility.getSyncCurrencies(getContext());

            if (currencyQueryStr.isEmpty()) {
                Log.d(LOG_TAG, "No currencies to sync");
                return;
            }

            Uri currencyUri = Uri.parse(FOREX_BASE_URL).buildUpon()
                    .appendQueryParameter(QUERY_PARAM, currencyQueryStr).build();

            URL url = new URL(currencyUri.toString());

            // Create the request to CurrencyConverterAPI, and open the connection
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setRequestMethod("GET");
            urlConnection.connect();

            // Read the input stream into a String
            InputStream inputStream = urlConnection.getInputStream();
            StringBuffer buffer = new StringBuffer();
            if (inputStream == null) {
                // Nothing to do.
                return;
            }
            reader = new BufferedReader(new InputStreamReader(inputStream));

            String line;
            while ((line = reader.readLine()) != null) {
                // Since it's JSON, adding a newline isn't necessary (it won't affect parsing)
                // But it does make debugging a *lot* easier if you print out the completed
                // buffer for debugging.
                buffer.append(line + "\n");
            }

            if (buffer.length() == 0) {
                // Stream was empty.  No point in parsing.
                setForexStatus(getContext(), FOREX_STATUS_SERVER_DOWN);
                return;
            }
            forexJsonStr = buffer.toString();
            getForexDataFromJson(forexJsonStr, currencyQueryStr);
        } catch (IOException e) {
            Log.e(LOG_TAG, "Error ", e);
            // If the code didn't successfully get the forex data, there's no point in attemping
            // to parse it.
            setForexStatus(getContext(), FOREX_STATUS_SERVER_DOWN);
        } catch (JSONException e) {
            Log.e(LOG_TAG, e.getMessage(), e);
            e.printStackTrace();
            setForexStatus(getContext(), FOREX_STATUS_SERVER_INVALID);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (final IOException e) {
                    Log.e(LOG_TAG, "Error closing stream", e);
                }
            }
        }

        // This will only happen if there was an error getting or parsing the forex.
        return;
    }

    /**
     * Take the String representing the complete forex in JSON Format and
     * pull out the data we need to construct the Strings needed for the wireframes.
     * <p/>
     * Fortunately parsing is easy:  constructor takes the JSON string and converts it
     * into an Object hierarchy for us.
     */
    private void getForexDataFromJson(String forexJsonStr, String currencyQuery) throws JSONException {

        // Now we have a String representing the complete forex in JSON Format.
        // Fortunately parsing is easy:  constructor takes the JSON string and converts it
        // into an Object hierarchy for us.

        // These are the names of the JSON objects that need to be extracted.
        final String OWM_RESULT = "results";
        final String OWM_RATE_FROM = "fr";
        final String OWM_RATE_TO = "to";
        final String OWM_RATE = "val";
        String[] currencies;
        boolean alertAvailable = false;

        try {
            if (mAlertData != null && mAlertData.getCurrencyFrom() != null && mAlertData.getCurrencyTo() != null) {
                alertAvailable = true;
            }

            JSONObject forexJson = new JSONObject(forexJsonStr);

            // do we have an error?
            if (!forexJson.has(OWM_RESULT)) {
                setForexStatus(getContext(), FOREX_STATUS_INVALID);
                return;
            }

            currencies = currencyQuery.split(",");

            JSONObject forexResultObject = forexJson.getJSONObject(OWM_RESULT);

            // OWM returns daily rates based upon the local time of the city that is being
            // asked for, which means that we need to know the GMT offset to translate this data
            // properly.

            // Since this data is also sent in-order and the first day is always the
            // current day, we're going to take advantage of that to get a nice
            // normalized UTC date for all of our rates.

            Time dayTime = new Time();
            dayTime.setToNow();

            // we start at the day returned by local time. Otherwise this is a mess.
            int julianDate = Time.getJulianDay(System.currentTimeMillis(), dayTime.gmtoff);

            // now we work exclusively in UTC
            dayTime = new Time();
            long dateTime = dayTime.setJulianDay(julianDate);

            // Insert the new rates information into the database
            Vector<ContentValues> cVVector = new Vector<>(currencies.length);

            for (int i = 0; i < currencies.length; i++) {

                JSONObject currencyObject = forexResultObject.getJSONObject(currencies[i]);
                String rate_from = currencyObject.getString(OWM_RATE_FROM);
                String rate_to = currencyObject.getString(OWM_RATE_TO);
                double result = currencyObject.getDouble(OWM_RATE);

                ContentValues forexValues = new ContentValues();

                forexValues.put(ForexContract.RateEntry.COLUMN_RATE_FROM_KEY, rate_from);
                forexValues.put(ForexContract.RateEntry.COLUMN_RATE_TO_KEY, rate_to);
                forexValues.put(ForexContract.RateEntry.COLUMN_RATE_DATE, dateTime);
                forexValues.put(ForexContract.RateEntry.COLUMN_RATE_VALUE, result);

                if (alertAvailable && mAlertData.getCurrencyFrom().getId().equals(rate_from)
                        && mAlertData.getCurrencyTo().getId().equals(rate_to)) {
                    mCurrentAlertRate = result;
                }

                cVVector.add(forexValues);
            }

            int inserted = 0;
            // add to database
            if (cVVector.size() > 0) {
                ContentValues[] cvArray = new ContentValues[cVVector.size()];
                cVVector.toArray(cvArray);
                getContext().getContentResolver().bulkInsert(ForexContract.RateEntry.CONTENT_URI, cvArray);

                // delete old data so we don't build up an endless history
                getContext().getContentResolver().delete(ForexContract.RateEntry.CONTENT_URI,
                        ForexContract.RateEntry.COLUMN_RATE_DATE + " <= ?",
                        new String[] { Long.toString(dayTime.setJulianDay(julianDate - FOREX_DAYS_TO_KEEP)) });

                setForexSyncDate(getContext(), System.currentTimeMillis());
                sendSyncBroadcast(FOREX_STATUS_OK);
                checkCurrencyData();
            }

            Log.d(LOG_TAG, "ForexSyncAdapter: Sync Complete. " + cVVector.size() + " Inserted");
            setForexStatus(getContext(), FOREX_STATUS_OK);

        } catch (JSONException e) {
            Log.e(LOG_TAG, e.getMessage(), e);
            e.printStackTrace();
            setForexStatus(getContext(), FOREX_STATUS_SERVER_INVALID);
        }
    }

    /**
     * Send sync broadcast so App and Widget can update the rate values immediately.
     *
     * @param status Status of the rate sync
     */
    private void sendSyncBroadcast(int status) {
        Context context = getContext();

        // Setting the package ensures that only components in our app will receive the broadcast
        Intent dataUpdatedIntent = new Intent(ACTION_DATA_UPDATED).setPackage(context.getPackageName())
                .putExtra(FOREX_DATA_STATUS, status);
        LocalBroadcastManager.getInstance(context).sendBroadcast(dataUpdatedIntent);
    }

    /**
     * This method request the Alert Service to run and validate the data to see if it needs
     * to alert the user about any currency rise or fall.
     */
    private void checkCurrencyData() {
        AlertService.startAlertService(getContext(), mCurrentAlertRate);
    }

}