com.skywomantechnology.app.guildviewer.sync.GuildViewerSyncAdapter.java Source code

Java tutorial

Introduction

Here is the source code for com.skywomantechnology.app.guildviewer.sync.GuildViewerSyncAdapter.java

Source

package com.skywomantechnology.app.guildviewer.sync;

/*
 * Guild Viewer is an Android app that allows users to view news feeds and news feed details
 * on a mobile device and while not logged into the game servers.
 *
 * Copyright 2014 Sky Woman Technology LLC
 *
 *    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.
 */

import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
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.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.widget.Toast;

import com.skywomantechnology.app.guildviewer.Constants;
import com.skywomantechnology.app.guildviewer.NewsListActivity;
import com.skywomantechnology.app.guildviewer.R;
import com.skywomantechnology.app.guildviewer.Utility;
import com.skywomantechnology.app.guildviewer.data.GuildViewerAchievement;
import com.skywomantechnology.app.guildviewer.data.GuildViewerContract;
import com.skywomantechnology.app.guildviewer.data.GuildViewerContract.GuildEntry;
import com.skywomantechnology.app.guildviewer.data.GuildViewerContract.ItemEntry;
import com.skywomantechnology.app.guildviewer.data.GuildViewerContract.MemberEntry;
import com.skywomantechnology.app.guildviewer.data.GuildViewerContract.NewsEntry;
import com.skywomantechnology.app.guildviewer.data.GuildViewerGuild;
import com.skywomantechnology.app.guildviewer.data.GuildViewerItem;
import com.skywomantechnology.app.guildviewer.data.GuildViewerMember;
import com.skywomantechnology.app.guildviewer.data.GuildViewerNewsItem;

import org.json.JSONArray;
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.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Vector;

/**
 * This class does the bulk of the work for getting the Guild News Feed data from the WOW API. It
 * sets up a periodic check for remote data from Blizzard's World of Warcraft API.
 * It allows the data to be synced on command. It retrieves and parses the JSON data, puts it in
 * local storage, and sends out a Notification when newer news items are found.
 *
 * @author Daun M. Davids
 * @version 1.05 August 30, 2014
 */
public class GuildViewerSyncAdapter extends AbstractThreadedSyncAdapter {

    //private final String LOG_TAG = GuildViewerSyncAdapter.class.getSimpleName();

    // keep track of our application environment context
    private final Context mContext;

    // Try to sync the data at approximately 20 minute intervals
    // it can range plus or minus 6ish minutes
    private static final int SYNC_INTERVAL = 60 * 20; //  20 minutes
    private static final int SYNC_FLEXTIME = SYNC_INTERVAL / 3;

    // indicates that the next sync should sync all news and ignore
    // the last sync timestamp checking
    public static final String SYNC_EXTRAS_SYNC_ALL = "sync_all_news";
    public static final String SYNC_EXTRAS_SYNC_MAX = "sync_max_value";
    public static final int SYNC_NO_MAX = -1;

    /**
     * Constructor.
     *
     * @param context
     *         current application environment context
     * @param autoInitialize
     *         if true then sync requests that have SYNC_EXTRAS_INITIALIZE set will be internally
     *         handled
     */
    public GuildViewerSyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);
        mContext = context;
    }

    /**
     * Sync the remote data immediately and use the last notification to
     * determine if records are already synced
     *
     * @param context
     *         current application environment context
     */
    public static void syncImmediately(Context context) {
        syncImmediately(context, false, SYNC_NO_MAX);
    }

    /**
     *  Sync the remote data immediately passing along sync data to
     *  the routines to determine how much data to sync
     *
     * @param context Activity context
     * @param syncAll if true process all news items within the date range,
     *                if false process normally by checking the timestamps
     * @param syncMax Max number of news items to process
     */
    public static void syncImmediately(Context context, boolean syncAll, int syncMax) {
        Bundle bundle = new Bundle();
        bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
        bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
        bundle.putBoolean(SYNC_EXTRAS_SYNC_ALL, syncAll);
        bundle.putInt(SYNC_EXTRAS_SYNC_MAX, syncMax);
        ContentResolver.requestSync(getSyncAccount(context), context.getString(R.string.content_authority), bundle);
    }

    /**
     * Set up the sync to run at periodic intervals.
     *
     * @param context
     *         current application environment context
     * @param syncInterval
     *         how often to sync in seconds
     * @param flexTime
     *         how many seconds can it run before syncInterval (inexact timer for versions greater
     *         than KITKAT
     */
    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).build();
            ContentResolver.requestSync(request);
        } else {
            ContentResolver.addPeriodicSync(account, authority, new Bundle(), syncInterval);
        }
    }

    /**
     * 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.
     *
     * @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 your <provider> element in the manifest,
            // then call context.setIsSyncable(account, AUTHORITY, 1) here.
            onAccountCreated(newAccount, context);
        }
        return newAccount;
    }

    /**
     * Configure a periodic sync of the data then Sync data immediately.
     * Display a Toast message to let the user know what is happening.
     *
     * @param newAccount
     *         account to configure sync on
     * @param context
     *         current application environment context
     */
    private static void onAccountCreated(Account newAccount, Context context) {

        configurePeriodicSync(context, SYNC_INTERVAL, SYNC_FLEXTIME);
        ContentResolver.setSyncAutomatically(newAccount, context.getString(R.string.content_authority), true);

        // sync the initial data
        syncImmediately(context);
        Toast.makeText(context, context.getString(R.string.toast_retrieving_data_message), Toast.LENGTH_LONG)
                .show();
    }

    /**
     * Initialize the SyncAdapter.
     *
     * @param context
     *         current application environment context
     */
    public static void initializeSyncAdapter(Context context) {
        getSyncAccount(context);
    }

    /**
     * When a sync occurs gets the JSON data from the API and then parses and processes it. Also
     * cleans old data from the storage so that the storage is maintained and does not keep growing
     * and growing.
     *
     * @param account
     *         the account that should be synced
     * @param bundle
     *         SyncAdapter-specific parameters
     * @param authority
     *         the authority of this sync request
     * @param provider
     *         a ContentProviderClient that points to the ContentProvider for this authority
     * @param syncResult
     *         SyncAdapter-specific parameters
     */
    @Override
    public void onPerformSync(Account account, Bundle bundle, String authority, ContentProviderClient provider,
            SyncResult syncResult) {
        // remove old records before we add new ones
        cleanUpOldNews(mContext);

        // add any new news
        int items = getNewsFromAPI();
        //Log.v(LOG_TAG, "Items actually added: " + Integer.toString(items));

        // update guild members
        getGuildMembersFromAPI();
    }

    /**
     * Retrieves the JSON data from the API for the Guild News Feed.
     *
     * @return number of news items obtained from API
     *
     * @see <a href="http://blizzard.github.io/api-wow-docs/">WOW API Documentation</a>
     */
    private int getNewsFromAPI() {

        // get the parameters we need to build the Uri
        String region = Utility.getRegion(mContext);
        String realm = Utility.getRealm(mContext);
        String guild = Utility.getGuildName(mContext);
        try {
            realm = URLEncoder.encode(realm, "UTF-8").replace(Constants.PLUS_SIGN, Constants.SPACE);
            guild = URLEncoder.encode(guild, "UTF-8").replace(Constants.PLUS_SIGN, Constants.SPACE);
        } catch (UnsupportedEncodingException e) {
            //do nothing so as not to crash app ... only no data will display
            e.printStackTrace();

        }

        // Construct the URL for the  query to the WOW APIs
        final String GUILD_NEWS_BASE_URL = "http://" + region + "/api/wow/guild/" + realm + "/" + guild + "?";

        Uri builtUri = Uri.parse(GUILD_NEWS_BASE_URL).buildUpon()
                .appendQueryParameter(Constants.FIELDS_PARAM, Constants.NEWS).build();
        //Log.v(LOG_TAG, "Built URI " + builtUri.toString());

        // get the JSON String and process the JSON objects created from the string
        return processJsonNews(getHTTPData(builtUri.toString()));
    }

    /**
     * Parse the JSON formatted data, process it, then store it locally.
     *
     * @param newsJsonStr
     *         JSON formatted string to parse and process
     * @return number of news items processed
     *
     * @see <a href="http://blizzard.github.io/api-wow-docs/">WOW API Documentation</a>
     */
    @SuppressWarnings("ConstantConditions")
    private int processJsonNews(String newsJsonStr) {

        int actualInsertCount = 0;
        // check to see if there is anything to do
        if (newsJsonStr == null)
            return 0;

        // These are the item keys that we want to extra from the JSON object

        // Guild data keys
        final String WOW_GUILD_LAST_MODIFIED = "lastModified";
        final String WOW_GUILD_NAME = "name";
        final String WOW_GUILD_REALM = "realm";
        final String WOW_GUILD_NEWS = "news";

        // News Item data keys
        final String WOW_NEWS_TYPE = "type";
        final String WOW_NEWS_TIMESTAMP = "timestamp";
        final String WOW_NEWS_CHARACTER = "character";
        final String WOW_NEWS_ITEM_ID = "itemId";
        final String WOW_NEWS_ACHIEVEMENT = "achievement";

        //Achievement data keys
        final String WOW_ACHIEVEMENT_DESCRIPTION = "description";
        final String WOW_ACHIEVEMENT_ICON = "icon";
        final String WOW_ACHIEVEMENT_TITLE = "title";

        // store the last sync timestamp and use it to keep the processing time to a minimum
        long mSavedTimestamp;

        // keep track of if we sent out a favorite character notification
        // this round... it will just send one and only the first one
        boolean isNotified = false;

        // keep track of if we sent a new record notification this round
        boolean alreadyNotified = false;

        // the region is not returned in the JSON data
        // so we use the same preference data that was used to get the JSON
        String region = Utility.getRegion(mContext);
        try {

            // create JSON object for the string
            JSONObject newsJson = new JSONObject(newsJsonStr);

            // create a news Item object to hold the relevant data from the JSON object
            GuildViewerNewsItem currentNews = new GuildViewerNewsItem();

            // setup the guild object
            GuildViewerGuild guild = new GuildViewerGuild();
            currentNews.setGuild(guild);
            guild.setName(newsJson.getString(WOW_GUILD_NAME));
            guild.setRegion(region);
            guild.setRealm(newsJson.getString(WOW_GUILD_REALM));

            // setup data used for notifications
            guild.setLastModified(newsJson.getLong(WOW_GUILD_LAST_MODIFIED));
            mSavedTimestamp = guild.getLastModified(); // default to WOW's modified date
            //Log.v(LOG_TAG, guild.toString());

            // now process the array of news items
            JSONArray newsArray = newsJson.getJSONArray(WOW_GUILD_NEWS);
            //Log.v(LOG_TAG, "Number of News Items in JSON:" + Integer.toString(newsArray.length()));

            // get the timestamps for both the last sync and for the furthest date that
            // we will process the news ... this allows us to only  process relevant news items
            //long lastNotificationTimestamp = Utility.getPreferenceForLastNotificationDate(mContext);
            long oldestNewsTimestamp = Utility
                    .getTimestampForDaysFromNow(Utility.getPreferenceForDaysToKeepNews(mContext));

            // Insert the new news items into the database
            Vector<ContentValues> cVVector = new Vector<ContentValues>(newsArray.length());
            for (int i = 0; i < newsArray.length(); i++) {

                // get the news item object from the JSON object
                JSONObject jsonNewsItem = newsArray.getJSONObject(i);

                // The only guaranteed fields from Blizzard are type and timestamp
                currentNews.setTimestamp(jsonNewsItem.getLong(WOW_NEWS_TIMESTAMP));

                // we can quit if we find one that is less than our last timestamp
                // because we know that we get the news items in date descending order
                // this will save us a lot of processing time and effort
                if (currentNews.getTimestamp() < oldestNewsTimestamp) {
                    //Log.v(LOG_TAG, "Stop processing. Current News is complete.");
                    break;
                }

                // format the dateTime string
                currentNews.setListFormattedDate(Utility.getReadableDateString(mContext, currentNews.getTimestamp(),
                        R.string.format_timestamp_list_view));

                // get the news item type because we need to know this to process correctly
                currentNews.setType(jsonNewsItem.getString(WOW_NEWS_TYPE));

                // character name may or may not be available in JSON object
                try {
                    currentNews.setCharacter(jsonNewsItem.getString(WOW_NEWS_CHARACTER));
                } catch (JSONException e) {
                    // This isn't a real exception because the character name is optional
                    // so catch it and keep it and reset the character name to be stored
                    currentNews.setCharacter("");
                }

                // item id may or may not be available in JSON object
                GuildViewerItem actualItem;
                try {
                    // try storage first
                    actualItem = checkForItemInStorage(jsonNewsItem.getLong(WOW_NEWS_ITEM_ID));
                    // if not found then got to the API to get it and store it
                    if (actualItem == null) {
                        actualItem = getItemFromAPI(jsonNewsItem.getLong(WOW_NEWS_ITEM_ID));
                    }
                    currentNews.setItem(actualItem);
                } catch (JSONException e) {
                    // We really can keep moving with this exception
                    // because the item id is optional so catch it and keep it
                    // if its a JSONException then its already been error level logged
                }

                // if the news type is an achievement then we look for the details
                // otherwise we can skip this processing
                if (Utility.containsAchievement(currentNews.getType())) {
                    GuildViewerAchievement achievement = new GuildViewerAchievement();
                    JSONObject achievementObj = jsonNewsItem.getJSONObject(WOW_NEWS_ACHIEVEMENT);
                    achievement.setDescription(achievementObj.getString(WOW_ACHIEVEMENT_DESCRIPTION));
                    achievement.setTitle(achievementObj.getString(WOW_ACHIEVEMENT_TITLE));
                    achievement.setIcon(achievementObj.getString(WOW_ACHIEVEMENT_ICON));
                    currentNews.setAchievement(achievement);
                }

                // all the relevant data is extracted so create a ContentValues object
                // to store it and add it to the array of objects to process later
                ContentValues newsListValues = createValuesObject(currentNews);
                cVVector.add(newsListValues);

                // we know the first entry is the newest of all the news so use this
                // to save us some processing and time.
                if (!alreadyNotified && cVVector.size() == 1) {
                    mSavedTimestamp = currentNews.getTimestamp();
                    // only notify on the action bar for the very first entry aka newest entry
                    notifyNews(mContext, currentNews, GUILD_VIEWER_NOTIFICATION_ID);
                    alreadyNotified = true;
                }
                // send a notification if the news is about a favorite character
                // but only send the very first one encountered in the news list
                // this will override the newest news notification
                else if (!isNotified && currentNews.getCharacter().toLowerCase()
                        .equals(Utility.getCharacter(mContext).toLowerCase())) {
                    notifyNews(mContext, currentNews, GUILD_VIEWER_FAVORITE_CHARACTER_NOTIFICATION);
                    isNotified = true;
                }
                //Log.v(LOG_TAG, currentNews.toString());

                //write out news in small groups to keep from list view from
                // looking like its loading forever and forever on large news lists
                if (cVVector.size() == Constants.MIN_NEWS_ITEMS_TO_LOAD) {
                    actualInsertCount += insertNews(cVVector);
                }
            }
            // We are done processing the JSON object
            // store any new and unique news records that were found and not yet stored
            actualInsertCount += insertNews(cVVector);

            // see if we need to update the latest timestamp information in the preference storage
            if (Utility.getPreferenceForLastNotificationDate(mContext) < mSavedTimestamp) {
                Utility.setPreferenceForLastNotificationDate(mContext, mSavedTimestamp);
            }
        } catch (JSONException e) {
            // if any JSON errors occurred log them and
            // for this app it is appropriate to just keep moving along... don't crash it!
            e.printStackTrace();
        }
        return actualInsertCount;
    }

    /**
     * Converts the ContentValue vector to an array and bulk inserts them
     * then clears out the vector array
     *
     * @param cVVector records to insert
     * @return int count of how many were actually inserted
     */
    private int insertNews(Vector<ContentValues> cVVector) {
        int insertCount = 0;
        if (cVVector == null || cVVector.isEmpty())
            return insertCount;

        int numRecordsToInsert = cVVector.size();
        if (numRecordsToInsert > 0) {
            // convert to an array for the bulk insert to work with
            ContentValues[] cvArray = new ContentValues[cVVector.size()];
            cVVector.toArray(cvArray);

            // inserts into the storage
            insertCount = mContext.getContentResolver().bulkInsert(NewsEntry.CONTENT_URI, cvArray);

            // clear out the loaded records so there are not duplicates
            cVVector.clear();
        }
        return insertCount;
    }

    /**
     * The item names and descriptions are a separate API call from the News Items. This method
     * obtains the information on the items that have been looted, crafted, and purchased.
     *
     * @param itemId
     *         id of item from news feed API to get from item API
     * @return JSON formatted string containing the item data
     *
     * @throws JSONException
     *         if any JSON object parsing fails
     * @see <a href="http://blizzard.github.io/api-wow-docs/">WOW API Documentation</a>
     */
    private GuildViewerItem getItemFromAPI(long itemId) throws JSONException {

        final String ITEM_LOOKUP_BASE_URL = "http://us.battle.net/api/wow/item/";
        Uri builtUri = Uri.parse(ITEM_LOOKUP_BASE_URL).buildUpon().appendPath(Long.toString(itemId)).build();
        //Log.v(LOG_TAG, "URI to get Item: " + builtUri.toString());

        return processItemDataFromJson(getHTTPData(builtUri.toString()));
    }

    /**
     * Parse the item information and store it if it is new and has not already been processed.
     *
     * @param wowItemJsonStr
     *         JSON formatted string
     * @return GuildViewerItem Object with the Item information parsed from the JSON string
     *
     * @throws JSONException
     *         if any JSON object parsing fails
     * @see <a href="http://blizzard.github.io/api-wow-docs/">WOW API Documentation</a>
     */
    private GuildViewerItem processItemDataFromJson(String wowItemJsonStr) throws JSONException {
        if (wowItemJsonStr == null)
            return null;

        // Item key values to parse from JSON string for Items
        final String WOW_ITEM_ID = "id";
        final String WOW_ITEM_NAME = "name";
        final String WOW_ITEM_ICON = "icon";
        final String WOW_ITEM_DESCRIPTION = "description";

        GuildViewerItem item = new GuildViewerItem();

        JSONObject itemJson = new JSONObject(wowItemJsonStr);
        item.setId(itemJson.getLong(WOW_ITEM_ID));
        item.setDescription(itemJson.getString(WOW_ITEM_DESCRIPTION));
        item.setName(itemJson.getString(WOW_ITEM_NAME));
        item.setIcon(itemJson.getString(WOW_ITEM_ICON));
        //Log.v(LOG_TAG, item.toString());

        // store this item locally
        return addItem(item);
    }

    /**
     * Performs a local storage lookup for the item using the item id.
     * This is the item from Blizzard not the id generated by the local storage.
     *
     * @param itemId
     *         of the item to find which is the item id given by the WOW API
     * @return GuildViewerItem with item details or null if not found
     */
    private GuildViewerItem checkForItemInStorage(long itemId) {
        GuildViewerItem item = null;

        // search the local storage for the item id
        Cursor cursor = mContext.getContentResolver().query(GuildViewerContract.ItemEntry.CONTENT_URI,
                new String[] { ItemEntry.COLUMN_NAME, ItemEntry.COLUMN_DESCRIPTION, ItemEntry.COLUMN_ICON },
                GuildViewerContract.ItemEntry.COLUMN_ITEM_ID + " = ?", new String[] { Long.toString(itemId) },
                null);

        // if it was found then put the item information in the GuildViewerItem object
        if (cursor.moveToFirst()) {
            item = new GuildViewerItem();
            item.setId(itemId);
            item.setName(cursor.getString(cursor.getColumnIndex(ItemEntry.COLUMN_NAME)));
            item.setDescription(cursor.getString(cursor.getColumnIndex(ItemEntry.COLUMN_DESCRIPTION)));
            item.setIcon(cursor.getString(cursor.getColumnIndex(ItemEntry.COLUMN_ICON)));
        }
        cursor.close();

        return item;
    }

    /**
     * Put item in storage if it does not already exist there
     *
     * @param item
     *         GuildViewerItem to store
     * @return item as added to the database
     *         or as it exists in the database null if original item parameter was null
     */
    private GuildViewerItem addItem(GuildViewerItem item) {
        if (item != null) {
            // check if the item exists in storage already
            GuildViewerItem found = checkForItemInStorage(item.getId());
            if (found != null) {
                return found;
            }
            // otherwise store the item information locally
            ContentValues itemValues = new ContentValues();
            itemValues.put(ItemEntry.COLUMN_ITEM_ID, item.getIdAsString());
            itemValues.put(ItemEntry.COLUMN_NAME, item.getName());
            itemValues.put(ItemEntry.COLUMN_ICON, item.getIcon());
            itemValues.put(ItemEntry.COLUMN_DESCRIPTION, item.getDescription());
            mContext.getContentResolver().insert(ItemEntry.CONTENT_URI, itemValues);
        }

        return item;
    }

    private int getGuildMembersFromAPI() {

        // get the parameters we need to build the Uri
        String region = Utility.getRegion(mContext);
        String realm = Utility.getRealm(mContext);
        String guild = Utility.getGuildName(mContext);
        try {
            realm = URLEncoder.encode(realm, "UTF-8").replace(Constants.PLUS_SIGN, Constants.SPACE);
            guild = URLEncoder.encode(guild, "UTF-8").replace(Constants.PLUS_SIGN, Constants.SPACE);
        } catch (UnsupportedEncodingException e) {
            // do nothing
        }

        // Construct the URL for the  query to the WOW APIs
        final String GUILD_NEWS_BASE_URL = "http://" + region + "/api/wow/guild/" + realm + "/" + guild + "?";

        Uri builtUri = Uri.parse(GUILD_NEWS_BASE_URL).buildUpon()
                .appendQueryParameter(Constants.FIELDS_PARAM, Constants.MEMBERS).build();
        //Log.v(LOG_TAG, "Built URI " + builtUri.toString());

        // get the JSON String and process the JSON objects created from the string
        return processGuildMembers(getHTTPData(builtUri.toString()));
    }

    private int processGuildMembers(String guildJsonStr) {

        int actualInsertCount = 0;
        // check to see if there is anything to do
        if (guildJsonStr == null)
            return 0;

        // These are the item keys that we want to extra from the JSON object

        // Guild data keys
        final String WOW_GUILD_LAST_MODIFIED = "lastModified";
        final String WOW_GUILD_NAME = "name";
        final String WOW_GUILD_REALM = "realm";
        final String WOW_GUILD_BATTLEGROUP = "battlegroup";
        final String WOW_GUILD_LEVEL = "level";
        final String WOW_GUILD_SIDE = "side";
        final String WOW_GUILD_POINTS = "achievementPoints";
        final String WOW_GUILD_MEMBERS = "members";

        // Member data keys
        final String WOW_MEMBER_CHARACTER = "character";
        final String WOW_MEMBER_RANK = "rank";

        // Member character data keys
        final String WOW_MEMBER_NAME = "name";
        final String WOW_MEMBER_CLASS = "class";
        final String WOW_MEMBER_RACE = "race";
        final String WOW_MEMBER_GENDER = "gender";
        final String WOW_MEMBER_LEVEL = "level";
        final String WOW_MEMBER_POINTS = "achievementPoints";
        final String WOW_MEMBER_THUMBNAIL = "thumbnail";

        // store the last guild update and use it to keep the processing time to a minimum
        long mSavedTimestamp = Utility.getPreferenceForLastGuildMemberUpdate(mContext);

        // the region is not returned in the JSON data
        // so we use the same preference data that was used to get the JSON
        String region = Utility.getRegion(mContext);
        try {

            // create JSON object for the string
            JSONObject guildJson = new JSONObject(guildJsonStr);

            long modifiedTimestamp = guildJson.getLong(WOW_GUILD_LAST_MODIFIED);
            if (modifiedTimestamp <= mSavedTimestamp) {
                // we can stop processing now because nothing has changes since we
                // last checked. Save some processing power
                return actualInsertCount;
            }
            // create a guild object to hold the relevant data from the JSON object
            GuildViewerGuild currentGuild = new GuildViewerGuild();

            // do a timestamp check to keep processing to minimum
            currentGuild.setLastModified(modifiedTimestamp);

            // save this time for the next round
            mSavedTimestamp = modifiedTimestamp; // default to WOW's modified date

            // setup the guild object
            currentGuild.setName(guildJson.getString(WOW_GUILD_NAME));
            currentGuild.setRegion(region);
            currentGuild.setRealm(guildJson.getString(WOW_GUILD_REALM));
            currentGuild.setBattlegroup(guildJson.getString(WOW_GUILD_BATTLEGROUP));
            currentGuild.setLevel(guildJson.getInt(WOW_GUILD_LEVEL));
            int side = guildJson.getInt(WOW_GUILD_SIDE);
            currentGuild.setSide(Utility.getGuildSide(mContext, side));
            currentGuild.setAchievementPoints(guildJson.getInt(WOW_GUILD_POINTS));

            //Add this to the database so that I know what to set the guild ID to in the members
            // But first delete the one that is in the database!!
            int deleted = mContext.getContentResolver().delete(GuildEntry.CONTENT_URI, null, null);
            //Log.v(LOG_TAG, "Deleted " + Integer.toString(deleted) + " Guilds from Database.");

            ContentValues currentGuildValues = createValuesObject(currentGuild);
            Uri guildUri = mContext.getContentResolver().insert(GuildEntry.CONTENT_URI, currentGuildValues);
            int guildId = GuildEntry.getGuildIdFromUri(guildUri);
            //Log.v(LOG_TAG, "New Guild Id is " + Integer.toString(guildId) );

            // now process the array of guild members
            JSONArray membersArray = guildJson.getJSONArray(WOW_GUILD_MEMBERS);
            //Log.v(LOG_TAG, "Number of Guild Members Found in JSON:" + Integer.toString(membersArray.length()));

            // Insert the new members into the database
            Vector<ContentValues> cVVector = new Vector<ContentValues>(membersArray.length());
            for (int i = 0; i < membersArray.length(); i++) {

                GuildViewerMember currentMember = new GuildViewerMember();

                // get the member object from the JSON object
                JSONObject jsonMember = membersArray.getJSONObject(i);
                // get the character object from the member object
                JSONObject characterJson = jsonMember.getJSONObject(WOW_MEMBER_CHARACTER);

                currentMember.setName(characterJson.getString(WOW_MEMBER_NAME));
                currentMember.setGuildId(guildId);
                currentMember.setLevel(characterJson.getInt(WOW_MEMBER_LEVEL));
                currentMember.setGender(Utility.getGender(mContext, characterJson.getInt(WOW_MEMBER_GENDER)));
                int raceId = characterJson.getInt(WOW_MEMBER_RACE);
                currentMember.setRace(Utility.getRace(mContext, raceId));
                currentMember.setCharacterClass(Utility.getClass(mContext, characterJson.getInt(WOW_MEMBER_CLASS)));
                currentMember.setSide(Utility.getSide(mContext, raceId));
                currentMember.setRank(jsonMember.getInt(WOW_MEMBER_RANK));
                currentMember.setAchievementPoints(characterJson.getInt(WOW_MEMBER_POINTS));
                currentMember.setThumbnail(characterJson.getString(WOW_MEMBER_THUMBNAIL));

                // all the relevant data is extracted so create a ContentValues object
                // to store it and add it to the array of objects to process later
                ContentValues memberListValues = createValuesObject(currentMember);
                cVVector.add(memberListValues);

                //Log.v(LOG_TAG, currentMember.toString());
            }

            // We are done processing the JSON object
            // store the new and unique news records that were found
            if (cVVector.size() > 0) {
                // convert to an array for the bulk insert to work with
                ContentValues[] cvArray = new ContentValues[cVVector.size()];
                cVVector.toArray(cvArray);
                // inserts into the storage
                actualInsertCount = mContext.getContentResolver().bulkInsert(MemberEntry.CONTENT_URI, cvArray);
            }

            // see if we need to update the latest timestamp information in the preference storage
            if (Utility.getPreferenceForLastGuildMemberUpdate(mContext) < mSavedTimestamp) {
                Utility.setPreferenceForLastGuildMemberUpdate(mContext, mSavedTimestamp);
            }
        } catch (JSONException e) {
            // if any JSON errors occurred log them and
            // for this app it is appropriate to just keep moving along... don't crash it!
        }
        return actualInsertCount;
    }

    /**
     *   Common HTTP functionality to perform a GET operation which reads
     *   in a string in JSON format
     *
     * @param apiUrl  Url request
     * @return String with the JSON data
     */
    public String getHTTPData(String apiUrl) {

        HttpURLConnection urlConnection = null;
        BufferedReader reader = null;
        String apiJsonStr = null;

        try {

            URL url = new URL(apiUrl);
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setRequestMethod("GET");
            urlConnection.connect();

            InputStream inputStream = urlConnection.getInputStream();
            StringBuffer buffer = new StringBuffer();
            if (inputStream == null) {
                return null;
            }
            reader = new BufferedReader(new InputStreamReader(inputStream));

            String line;
            while ((line = reader.readLine()) != null) {
                buffer.append(line + "\n");
            }
            if (buffer.length() == 0) {
                // Stream was empty.  No point in parsing.
                return null;
            }
            apiJsonStr = buffer.toString();
        } catch (IOException e) {
            //Log.e(LOG_TAG, "Error ", e);
            return null;
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (final IOException e) {
                    //Log.e(LOG_TAG, "Error closing stream", e);
                }
            }
        }
        return apiJsonStr;
    }

    /**
     * Map a GuildViewerNewsItem into a ContentValues item
     *
     * @param news GuildViewerNewsItem to put in ContentValues
     * @return ContentValues object with guild viewer news item data
     */
    private ContentValues createValuesObject(GuildViewerNewsItem news) {
        ContentValues cv = new ContentValues();
        cv.put(NewsEntry.COLUMN_REGION, news.getGuild().getRegion());
        cv.put(NewsEntry.COLUMN_REALM, news.getGuild().getRealm());
        cv.put(NewsEntry.COLUMN_GUILD, news.getGuild().getName());
        cv.put(NewsEntry.COLUMN_TYPE, news.getType());
        cv.put(NewsEntry.COLUMN_TIMESTAMP, news.getTimestamp());
        cv.put(NewsEntry.COLUMN_CHARACTER, news.getCharacter());

        boolean gotNews = (news.getItem() != null);
        cv.put(NewsEntry.COLUMN_ITEM_ID, (gotNews ? news.getItem().getIdAsString() : ""));
        cv.put(NewsEntry.COLUMN_ITEM_NAME, (gotNews ? news.getItem().getName() : ""));
        cv.put(NewsEntry.COLUMN_ITEM_DESCRIPTION, (gotNews ? news.getItem().getDescription() : ""));
        cv.put(NewsEntry.COLUMN_ITEM_ICON, (gotNews ? news.getItem().getIcon() : ""));

        boolean gotAchievement = (news.getAchievement() != null);
        cv.put(NewsEntry.COLUMN_ACHIEVEMENT_DESCRIPTION,
                (gotAchievement ? news.getAchievement().getDescription() : ""));
        cv.put(NewsEntry.COLUMN_ACHIEVEMENT_TITLE, (gotAchievement ? news.getAchievement().getTitle() : ""));
        cv.put(NewsEntry.COLUMN_ACHIEVEMENT_ICON, (gotAchievement ? news.getAchievement().getIcon() : ""));
        return cv;
    }

    /**
     * Copy Guild object to ContentValues
     *
     * @param guild to put in ContentValues
     * @return ContentValues with Guild information
     */
    private ContentValues createValuesObject(GuildViewerGuild guild) {
        ContentValues cv = new ContentValues();
        cv.put(GuildEntry.COLUMN_REGION, guild.getRegion());
        cv.put(GuildEntry.COLUMN_REALM, guild.getRealm());
        cv.put(GuildEntry.COLUMN_NAME, guild.getName());
        cv.put(GuildEntry.COLUMN_BATTLEGROUP, guild.getBattlegroup());
        cv.put(GuildEntry.COLUMN_LAST_MODIFIED, guild.getLastModified());
        cv.put(GuildEntry.COLUMN_LEVEL, guild.getLevel());
        cv.put(GuildEntry.COLUMN_POINTS, guild.getAchievementPoints());
        cv.put(GuildEntry.COLUMN_SIDE, guild.getSide());
        return cv;
    }

    /**
     * Copy Member object to ContentValues
     * @param member  to copy
     * @return ContentValues with Member information
     */
    private ContentValues createValuesObject(GuildViewerMember member) {
        ContentValues cv = new ContentValues();
        cv.put(MemberEntry.COLUMN_NAME, member.getName());
        cv.put(MemberEntry.COLUMN_LEVEL, member.getLevel());
        cv.put(MemberEntry.COLUMN_POINTS, member.getAchievementPoints());
        cv.put(MemberEntry.COLUMN_SIDE, member.getSide());
        cv.put(MemberEntry.COLUMN_RANK, member.getRank());
        cv.put(MemberEntry.COLUMN_RACE, member.getRace());
        cv.put(MemberEntry.COLUMN_THUMBNAIL, member.getThumbnail());
        cv.put(MemberEntry.COLUMN_CLASS, member.getCharacterClass());
        cv.put(MemberEntry.COLUMN_GENDER, member.getGender());
        cv.put(MemberEntry.COLUMN_GUILD_ID, member.getGuildId());
        return cv;
    }

    public static final int GUILD_VIEWER_NOTIFICATION_ID = 1111;
    public static final int GUILD_VIEWER_FAVORITE_CHARACTER_NOTIFICATION = 2222;

    /**
     * Handles setting up the notification.
     * There are two situation in which we send a notification
     * First if there is more recent news than the last sync
     * Second if the news is concerned with the favorite character
     * @param context to use for getting preference information
     * @param newsItem  contains the newsItem information used to build the notification
     * @param notification_id identifier for the notification
     */
    private void notifyNews(Context context, GuildViewerNewsItem newsItem, int notification_id) {

        // determine if we should send a notification
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        boolean notificationsEnabled = prefs.getBoolean(context.getString(R.string.pref_enable_notifications_key),
                Boolean.parseBoolean(context.getString(R.string.pref_enable_notifications_default)));
        long lastNotification = prefs.getLong(context.getString(R.string.pref_last_notification), 0L);
        boolean timeToNotify = newsItem.getTimestamp() > lastNotification;

        // we can notify for both new news and for favorite character news
        if (notificationsEnabled
                && (timeToNotify || notification_id == GUILD_VIEWER_FAVORITE_CHARACTER_NOTIFICATION)) {

            // build the information to put into the notification
            int iconId = R.drawable.ic_launcher;
            String title = context.getString(R.string.app_name);

            String news = String.format("%s %s %s", newsItem.getCharacter(),
                    Utility.getNewsTypeVerb(context, newsItem.getType()),
                    Utility.containsItem(newsItem.getType())
                            ? ((newsItem.getItem() != null) ? newsItem.getItem().getName() : "")
                            : ((newsItem.getAchievement() != null) ? newsItem.getAchievement().getTitle() : ""));
            String contentText = String.format(context.getString(R.string.format_notification), news);

            // set the notification to clear after a click
            NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context).setSmallIcon(iconId)
                    .setContentTitle(title).setContentText(contentText).setOnlyAlertOnce(true).setAutoCancel(true);

            // Open the app when the user clicks on the notification.
            Intent resultIntent = new Intent(context, NewsListActivity.class);
            TaskStackBuilder stackBuilder = TaskStackBuilder.create(context).addParentStack(NewsListActivity.class)
                    .addNextIntent(resultIntent);
            PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

            mBuilder.setContentIntent(resultPendingIntent);

            // we can notify for two reasons but lets just always have one notification at a time
            NotificationManager mNotificationManager = (NotificationManager) context
                    .getSystemService(Context.NOTIFICATION_SERVICE);
            mNotificationManager.notify(GUILD_VIEWER_NOTIFICATION_ID, mBuilder.build());
        }
    }

    /**
     * Removes data from the database if older than the preference WOW only sends a weeks worth
     * anyhow so the preference is limited to 1-7 days of news *
     *
     * @param context  context to use for deleting the old news entries
     * @return int with number of items deleted
     */
    private int cleanUpOldNews(Context context) {
        String deleteTimestamp = Long
                .toString(Utility.getTimestampForDaysFromNow(Utility.getPreferenceForDaysToKeepNews(context)));
        String selection = NewsEntry.COLUMN_TIMESTAMP + " < " + deleteTimestamp;
        return context.getContentResolver().delete(NewsEntry.CONTENT_URI, selection, null);
    }
}