Java tutorial
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); } }