com.akop.bach.parser.PsnEuParser.java Source code

Java tutorial

Introduction

Here is the source code for com.akop.bach.parser.PsnEuParser.java

Source

/*
 * PsnEuParser.java 
 * Copyright (C) 2010-2014 Akop Karapetyan
 *
 * This file is part of Spark 360, the online gaming service client.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
 *  02111-1307  USA.
 *
 */

package com.akop.bach.parser;

import java.io.IOException;
import java.net.URL;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.NameValuePair;
import org.apache.http.params.HttpParams;

import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;

import com.akop.bach.App;
import com.akop.bach.BasicAccount;
import com.akop.bach.PSN;
import com.akop.bach.PSN.ComparedGameInfo;
import com.akop.bach.PSN.ComparedTrophyInfo;
import com.akop.bach.PSN.Friends;
import com.akop.bach.PSN.GameCatalogItem;
import com.akop.bach.PSN.GameCatalogItemDetails;
import com.akop.bach.PSN.GameCatalogList;
import com.akop.bach.PSN.GamerProfileInfo;
import com.akop.bach.PSN.Games;
import com.akop.bach.PSN.Profiles;
import com.akop.bach.PSN.Trophies;
import com.akop.bach.Preferences;
import com.akop.bach.PsnAccount;
import com.akop.bach.R;
import com.akop.bach.util.rss.RssChannel;
import com.akop.bach.util.rss.RssHandler;

public class PsnEuParser extends PsnParser {
    protected static final String URL_BLOG = "http://feeds.feedburner.com/SCEEBlog?format=xml";

    private static final String LARGE_TROPHY_ICON_PREFIX = "http://trophy01.np.community.playstation.net";
    private static final String LARGE_AVATAR_ICON_PREFIX = "http://static-resource.np.community.playstation.net";

    private static final String URL_RETURN_LOGIN = "https://secure.eu.playstation.com/sign-in/confirmation/";

    private static final String URL_PROFILE_SUMMARY = "https://secure.eu.playstation.com/psn/mypsn/";
    private static final String URL_GAMES = "http://uk.playstation.com/psn/mypsn/ajax/trophies/?startIndex=%1$d&sortBy=recent";
    private static final String URL_TROPHIES_f = "http://uk.playstation.com/psn/mypsn/trophies/detail/?title=%1$s";
    private static final String URL_FRIENDS = "http://uk.playstation.com/psn/mypsn/friends/";
    //"https://secure.eu.playstation.com/psn/mypsn/friends/";
    private static final String URL_FRIENDS_AJAX = "http://uk.playstation.com/ajax/mypsn/friend/presence/";
    //"https://secure.eu.playstation.com/ajax/mypsn/friend/presence/";
    private static final String URL_COMPARE_GAMES_f = "http://uk.playstation.com/psn/mypsn/ajax/trophies-compare/friend/add/?onlineid=%s&endIndex=999&sortBy=recent";
    private static final String URL_COMPARE_TROPHIES_f = "http://uk.playstation.com/psn/mypsn/trophies/detail/?title=%s";
    private static final String URL_FRIEND_SUMMARY_f = "http://uk.playstation.com/psn/mypsn/trophies-compare/?friend=%1$s";
    //"https://secure.eu.playstation.com/psn/mypsn/trophies-compare/?friend=%1$s";

    private static final String URL_GAME_CATALOG = "http://uk.playstation.com/ajax/games-hub/";

    private static final Pattern PATTERN_SIGN_OUT_LINK = Pattern.compile("data-aa-evt-name=\"sign out\"");

    private static final Pattern PATTERN_ONLINE_ID = Pattern
            .compile("<div class=\"user-info\">\\s+<h2>\\s+(\\S+)\\s+</h2>\\s+<h3>\\s+<strong>");

    private static final Pattern PATTERN_INFOBAR_ITEM = Pattern.compile(
            "<div class=\"wrap\">\\s+<h4>[^<]+</h4>\\s+<div class=\"[^\"]+\"(?: style=\"[^\"]+\")?>\\s*([^<\\s]+)\\s*</div>");

    private static final Pattern PATTERN_PROGRESS = Pattern
            .compile("<div class=\"level-percentage\">\\s*(\\d+)\\s*<span>");
    private static final Pattern PATTERN_AVATAR_URL = Pattern
            .compile("<img alt=\"[^\"]+\" class=\"avatar\" src=\"([^\"]*)\"", Pattern.DOTALL);

    private static final Pattern PATTERN_IS_PLUS = Pattern.compile("<img\\s+alt=\"[^\"]+\"\\s+class=\"psp-logo\"");

    private static final Pattern PATTERN_GAMES = Pattern.compile("<li class=\"tile\">(.*?)</div>\\s+</li>",
            Pattern.DOTALL);

    private static final Pattern PATTERN_GAME_UID = Pattern
            .compile("<a class=\"tile-link [^\"]+\" href=\"[^=]*=(\\d+)\">");
    private static final Pattern PATTERN_GAME_PROGRESS = Pattern.compile("<span>(\\d+)%</span>");
    private static final Pattern PATTERN_GAME_TROPHIES = Pattern.compile("<li class=\"([^\"]+)\">([^<]+)</li>");
    private static final Pattern PATTERN_GAME_TITLE = Pattern
            .compile("<h2 class=\"clearfix title\">\\s+(\\S[^\\n\\r]+)\\r?\\n\\s+</h2>");
    private static final Pattern PATTERN_GAME_ICON = Pattern.compile("<img src=\"([^\"]*)\"");
    private static final Pattern PATTERN_GAME_MORE = Pattern.compile("class=\"more-link\">");

    private static final Pattern PATTERN_TROPHIES = Pattern
            .compile("<tr>\\s+<td class=\"trophy-unlocked-([^\"]+)\">(.*?)</tr>", Pattern.DOTALL);
    private static final Pattern PATTERN_TROPHY_TITLE = Pattern
            .compile("<h1 class=\"trophy_name ([^\"]+)\">\\s+(\\S[^\\n\\r]+)\\r?\\n\\s+</h1>");
    private static final Pattern PATTERN_TROPHY_DESCRIPTION = Pattern
            .compile("<h2>\\s+(\\S[^\\n\\r]+)\\r?\\n\\s+</h2>");
    private static final Pattern PATTERN_TROPHY_ICON = Pattern.compile("<img src=\"([^\"]*)\"");

    private static final Pattern PATTERN_FRIENDS = Pattern.compile("<psn_friend>(.*?)</psn_friend>",
            Pattern.DOTALL);
    private static final Pattern PATTERN_FRIEND_ONLINE_ID = Pattern.compile("<onlineid>([^<]*)</");
    private static final Pattern PATTERN_FRIEND_LEVEL = Pattern.compile("<level>(\\d+)</");
    private static final Pattern PATTERN_FRIEND_AVATAR = Pattern.compile("<current_avatar>([^<]+)</");
    private static final Pattern PATTERN_FRIEND_TROPHY = Pattern.compile("<(platinum|gold|silver|bronze)>(\\d+)</");
    private static final Pattern PATTERN_FRIEND_PLAYING = Pattern.compile("<current_game>([^<]*)</");
    private static final Pattern PATTERN_FRIEND_STATUS = Pattern.compile("<current_presence>([^<]*)</");
    private static final Pattern PATTERN_FRIEND_COMMENT = Pattern.compile("<comment>([^<]*)</");
    private static final Pattern PATTERN_FRIEND_IS_PLUS = Pattern.compile("<playstationplus>([^<]*)</");

    private static final Pattern PATTERN_FRIENDS_PENDING = Pattern.compile(
            "class=\"friendPresence-pending[^\"]*\"[^>]*>\\s*</td>\\s*<td[^>]*>\\s*(\\S+)\\s*<div", Pattern.DOTALL);

    private static final Pattern PATTERN_FRIEND_SUMMARY = Pattern
            .compile(" id=\"trophySummaryContainer\"[^>]*>(.*?)</div>\\s*</div>\\s*</div>\\s*</div>\\s*</div>");
    private static final Pattern PATTERN_FRIEND_SUMMARY_ONLINE_ID = Pattern.compile("<strong>([^<]*)</strong>");
    private static final Pattern PATTERN_FRIEND_SUMMARY_AVATAR = Pattern.compile("<img src=\"([^\"]*)\"");
    private static final Pattern PATTERN_FRIEND_SUMMARY_LEVEL = Pattern
            .compile("<p class=\"summary level\">(\\d+)</p>");
    private static final Pattern PATTERN_FRIEND_SUMMARY_PROGRESS = Pattern
            .compile("<p class=\"percentage\">(\\d+)%</p>");
    private static final Pattern PATTERN_FRIEND_SUMMARY_TROPHIES = Pattern
            .compile("<strong id=\"fl(Bronze|Silver|Gold|Platinum)Trophies\">(\\d+)\\s*</strong>");
    private static final Pattern PATTERN_FRIEND_SUMMARY_IS_PLUS = Pattern
            .compile("<div class=\"avatarPlayStationPlus\">");

    private static final Pattern PATTERN_COMPARED_GAMES = Pattern
            .compile("<tr id=\"([^\"]+)\" data-ajax-endcount=\"[^\"]*\">(.*?)</tr>", Pattern.DOTALL);
    private static final Pattern PATTERN_COMPARE_GAME_TITLE = Pattern
            .compile("</span>\\s+(\\S[^\\n\\r]+)\\r?\\n\\s+</div>\\s+</a>");
    private static final Pattern PATTERN_COMPARE_GAME_ICON = Pattern
            .compile("<div class=\"img-bkg\" style=\"background-image:url\\('([^']*)'");
    private static final Pattern PATTERN_COMPARE_USERNAME = Pattern
            .compile("<h4 class=\"user-name\">([^<]*)<span>");
    private static final Pattern PATTERN_COMPARE_AVATAR_URL = Pattern
            .compile("<img class=\"avatar-image\" src=\"([^\"]+)\"");
    private static final Pattern PATTERN_COMPARE_GAME_PLAYER = Pattern.compile("<td>(.*?)</td>", Pattern.DOTALL);
    private static final Pattern PATTERN_COMPARE_GAME_TROPHIES = Pattern.compile("<li>(\\d+)</li>");
    private static final Pattern PATTERN_COMPARE_GAME_PROGRESS = Pattern.compile("<span>(\\d+)%</span>");

    private static final Pattern PATTERN_COMPARED_TROPHIES = Pattern.compile("<tr>\\s+(.*?)\\s+</tr>",
            Pattern.DOTALL);
    private static final Pattern PATTERN_COMPARED_TROPHY_COLUMN = Pattern
            .compile("<t[dh]( class=\"([^\"]+)\")?>\\s+(.*?)\\s+</t[dh]>", Pattern.DOTALL);
    private static final Pattern PATTERN_COMPARED_FRIEND_ID = Pattern
            .compile("<h4 class=\"user-name\">([^<]+)</h4>");
    private static final Pattern PATTERN_COMPARED_TROPHY_ICON = Pattern.compile("<img src=\"([^\"]+)\"");
    private static final Pattern PATTERN_COMPARED_TROPHY_TITLE = Pattern
            .compile("<h1 class=\"trophy_name ([^\"]+)\">\\s+(\\S[^\\n\\r]+)\\r?\\n\\s+</h1>");
    private static final Pattern PATTERN_COMPARED_TROPHY_DESCRIPTION = Pattern
            .compile("<h2>\\s+(\\S[^\\n\\r]+)\\r?\\n\\s+</h2>");
    private static final Pattern PATTERN_COMPARED_TROPHY_IS_LOCKED = Pattern.compile("<span class=\"locked\"");

    private static final Pattern PATTERN_GAME_CATALOG_ITEMS = Pattern
            .compile("<game_result content_id=\"[^\"]*\">(.*?)</game_result>", Pattern.DOTALL);
    private static final Pattern PATTERN_GAME_CATALOG_STATS = Pattern
            .compile("<game_results page_current=\"(\\d+)\" page_total=\"(\\d+)\">");

    private static final Pattern PATTERN_GAME_CATALOG_NODE = Pattern.compile("<([a-z_]+)[^>]*>([^<]*)</[a-z_]+>");

    private static final Pattern PATTERN_GAME_CATALOG_DETAIL_DESC = Pattern
            .compile("<div id=\"content\" class=\"\">(.*?)</div>\\s*</div>", Pattern.DOTALL);
    private static final Pattern PATTERN_GAME_CATALOG_DETAIL_DESC_ALT = Pattern.compile(
            "<div class=\"gameInfo\">\\s*<div class=\"info\" style=\"[^\"]+\">(.*?)</div>\\s*</div>",
            Pattern.DOTALL);

    public PsnEuParser(Context context) {
        super(context);
    }

    @Override
    protected boolean onAuthenticate(BasicAccount account) throws IOException, ParserException {
        PsnAccount psnAccount = (PsnAccount) account;

        String password = psnAccount.getPassword();
        if (password == null)
            throw new ParserException(mContext.getString(R.string.decryption_error));

        HttpParams params = mHttpClient.getParams();

        // Prepare POSTDATA
        List<NameValuePair> inputs = new ArrayList<NameValuePair>(3);

        addValue(inputs, "j_username", psnAccount.getEmailAddress());
        addValue(inputs, "j_password", password);
        addValue(inputs, "returnURL", URL_RETURN_LOGIN);

        // Enable redirection (max 1)
        params.setParameter("http.protocol.max-redirects", 3);

        // 1. Post authentication data
        String page = getResponse(URL_LOGIN, inputs);

        // Disable redirection
        params.setParameter("http.protocol.max-redirects", 1);

        // Get redirection URL
        Matcher m = PATTERN_SIGN_OUT_LINK.matcher(page);
        if (!m.find()) {
            if (App.getConfig().logToConsole())
                App.logv("onAuthEU: Redir URL not found");

            String outageMessage;
            if ((outageMessage = getOutageMessage(page)) != null)
                throw new ParserException(outageMessage);

            return false;
        }

        return true;
    }

    @Override
    protected String getSessionFile(BasicAccount account) {
        return account.getUuid() + ".eu.session";
    }

    @Override
    protected ContentValues parseSummaryData(PsnAccount account) throws IOException, ParserException {
        String page = getResponse(URL_PROFILE_SUMMARY);
        ContentValues cv = new ContentValues(10);
        Matcher m;

        long started = System.currentTimeMillis();

        if (!(m = PATTERN_ONLINE_ID.matcher(page)).find())
            throw new ParserException(mContext, R.string.error_online_id_not_detected);

        cv.put(Profiles.ONLINE_ID, htmlDecode(m.group(1)));

        int memberType = PSN.MEMBER_TYPE_FREE;
        if (PATTERN_IS_PLUS.matcher(page).find())
            memberType = PSN.MEMBER_TYPE_PLUS;

        int level = 0;
        int trophiesPlat = 0;
        int trophiesGold = 0;
        int trophiesSilver = 0;
        int trophiesBronze = 0;

        Matcher infoBarMatcher = PATTERN_INFOBAR_ITEM.matcher(page);
        for (int i = 0; infoBarMatcher.find(); i++) {
            int value = Integer.parseInt(infoBarMatcher.group(1));
            if (i == 0)
                level = value;
            else if (i == 2)
                trophiesBronze = value;
            else if (i == 3)
                trophiesSilver = value;
            else if (i == 4)
                trophiesGold = value;
            else if (i == 5)
                trophiesPlat = value;
        }

        int progress = 0;
        if ((m = PATTERN_PROGRESS.matcher(page)).find())
            progress = Integer.parseInt(m.group(1));

        cv.put(Profiles.LEVEL, level);
        cv.put(Profiles.MEMBER_TYPE, memberType);
        cv.put(Profiles.PROGRESS, progress);
        cv.put(Profiles.TROPHIES_PLATINUM, trophiesPlat);
        cv.put(Profiles.TROPHIES_GOLD, trophiesGold);
        cv.put(Profiles.TROPHIES_SILVER, trophiesSilver);
        cv.put(Profiles.TROPHIES_BRONZE, trophiesBronze);

        String iconUrl = null;
        if ((m = PATTERN_AVATAR_URL.matcher(page)).find())
            iconUrl = getLargeAvatarIcon(resolveImageUrl(URL_PROFILE_SUMMARY, m.group(1)));

        cv.put(Profiles.ICON_URL, iconUrl);

        if (App.getConfig().logToConsole())
            started = displayTimeTaken("parseSummaryData processing", started);

        return cv;
    }

    private boolean parseGamePage(int startIndex, String page, List<ContentValues> cvList) {
        Matcher m;
        Matcher gameMatcher = PATTERN_GAMES.matcher(page);

        for (int i = 0; gameMatcher.find(); i++) {
            String gameContent = gameMatcher.group(1);

            String gameUid;
            int gameProgress;
            String gameTitle = null;
            String gameIcon = null;

            int bronze = 0;
            int silver = 0;
            int gold = 0;
            int platinum = 0;

            if (!(m = PATTERN_GAME_TROPHIES.matcher(gameContent)).find())
                continue;

            bronze = Integer.parseInt(m.group(2));

            if (!m.find())
                continue;

            silver = Integer.parseInt(m.group(2));

            if (!m.find())
                continue;

            gold = Integer.parseInt(m.group(2));

            if (!m.find())
                continue;

            platinum = Integer.parseInt(m.group(2));

            m = PATTERN_GAME_UID.matcher(gameContent);
            if (!m.find())
                continue;

            gameUid = m.group(1);

            m = PATTERN_GAME_PROGRESS.matcher(gameContent);
            if (!m.find())
                continue;

            gameProgress = Integer.parseInt(m.group(1));

            m = PATTERN_GAME_TITLE.matcher(gameContent);
            if (m.find())
                gameTitle = htmlDecode(m.group(1));

            m = PATTERN_GAME_ICON.matcher(gameContent);
            if (m.find())
                gameIcon = getLargeTrophyIcon(resolveImageUrl(URL_GAMES, m.group(1)));

            ContentValues cv = new ContentValues(15);

            cv.put(Games.TITLE, gameTitle);
            cv.put(Games.UID, gameUid);
            cv.put(Games.ICON_URL, gameIcon);
            cv.put(Games.PROGRESS, gameProgress);
            cv.put(Games.SORT_ORDER, i + startIndex);
            cv.put(Games.UNLOCKED_PLATINUM, platinum);
            cv.put(Games.UNLOCKED_GOLD, gold);
            cv.put(Games.UNLOCKED_SILVER, silver);
            cv.put(Games.UNLOCKED_BRONZE, bronze);
            App.logv(gameTitle);
            cvList.add(cv);
        }

        return PATTERN_GAME_MORE.matcher(page).find();
    }

    @SuppressLint("DefaultLocale")
    @Override
    protected void parseGames(PsnAccount account) throws ParserException, IOException {
        long started = System.currentTimeMillis();
        boolean keepGoing = true;
        List<ContentValues> cvList = new ArrayList<ContentValues>();

        for (int startIndex = 0; keepGoing; startIndex += 16) {
            String url = String.format(URL_GAMES, startIndex);
            String page = getResponse(url);

            keepGoing = parseGamePage(startIndex, page, cvList);
        }

        final long accountId = account.getId();
        ContentResolver cr = mContext.getContentResolver();
        String[] queryParams = new String[1];
        Cursor c;
        long updated = System.currentTimeMillis();
        List<ContentValues> newCvs = new ArrayList<ContentValues>(100);

        // Check to see if we already have a record of this game
        for (ContentValues cv : cvList) {
            queryParams[0] = cv.getAsString(Games.UID);
            c = cr.query(Games.CONTENT_URI, GAMES_PROJECTION,
                    Games.ACCOUNT_ID + "=" + accountId + " AND " + Games.UID + "=?", queryParams, null);

            try {
                if (c == null || !c.moveToFirst()) // New game
                {
                    cv.put(Games.ACCOUNT_ID, accountId);
                    cv.put(Games.TROPHIES_DIRTY, 1);
                    cv.put(Games.LAST_UPDATED, updated);

                    newCvs.add(cv);
                } else // Existing game
                {
                    boolean isDirty = false;
                    long gameId = c.getLong(COLUMN_GAME_ID);

                    if (c.getInt(COLUMN_GAME_PROGRESS) != cv.getAsInteger(Games.PROGRESS))
                        isDirty = true;
                    if (c.getInt(COLUMN_GAME_BRONZE) != cv.getAsInteger(Games.UNLOCKED_BRONZE))
                        isDirty = true;
                    if (c.getInt(COLUMN_GAME_SILVER) != cv.getAsInteger(Games.UNLOCKED_SILVER))
                        isDirty = true;
                    if (c.getInt(COLUMN_GAME_GOLD) != cv.getAsInteger(Games.UNLOCKED_GOLD))
                        isDirty = true;
                    if (c.getInt(COLUMN_GAME_PLATINUM) != cv.getAsInteger(Games.UNLOCKED_PLATINUM))
                        isDirty = true;

                    if (isDirty)
                        cv.put(Games.TROPHIES_DIRTY, 1);

                    cv.put(Games.LAST_UPDATED, updated);

                    cr.update(Games.CONTENT_URI, cv, Games._ID + "=" + gameId, null);
                }
            } finally {
                if (c != null)
                    c.close();
            }
        }

        if (App.getConfig().logToConsole())
            started = displayTimeTaken("Game page processing", started);

        if (newCvs.size() > 0) {
            ContentValues[] cvs = new ContentValues[newCvs.size()];
            newCvs.toArray(cvs);

            cr.bulkInsert(Games.CONTENT_URI, cvs);

            if (App.getConfig().logToConsole())
                displayTimeTaken("Game page insertion", started);
        }

        account.refresh(Preferences.get(mContext));
        account.setLastGameUpdate(System.currentTimeMillis());
        account.save(Preferences.get(mContext));

        cr.notifyChange(Games.CONTENT_URI, null);
    }

    @SuppressLint("DefaultLocale")
    @Override
    protected void parseTrophies(PsnAccount account, long gameId) throws ParserException, IOException {
        // Find game record in local DB
        ContentResolver cr = mContext.getContentResolver();

        String gameUid = Games.getUid(mContext, gameId);

        String url = String.format(URL_TROPHIES_f, gameUid);
        String page = getResponse(url);

        int index = 0;
        long started = System.currentTimeMillis();

        Matcher trophies = PATTERN_TROPHIES.matcher(page);
        Matcher m;

        List<ContentValues> cvList = new ArrayList<ContentValues>(100);
        while (trophies.find()) {
            boolean trophyUnlocked = "true".equalsIgnoreCase(trophies.group(1));
            String trophyContent = trophies.group(2);

            String iconUrl = null;
            if ((m = PATTERN_TROPHY_ICON.matcher(trophyContent)).find())
                iconUrl = getLargeTrophyIcon(resolveImageUrl(url, m.group(1)));

            String title = mContext.getString(R.string.secret_trophy);
            String description = mContext.getString(R.string.this_is_a_secret_trophy);

            if (!(m = PATTERN_TROPHY_TITLE.matcher(trophyContent)).find())
                continue;

            int type = PSN.TROPHY_SECRET;
            boolean isSecret = false;
            String trophyType = m.group(1).toUpperCase();
            if (trophyType.equals("BRONZE"))
                type = PSN.TROPHY_BRONZE;
            else if (trophyType.equals("SILVER"))
                type = PSN.TROPHY_SILVER;
            else if (trophyType.equals("GOLD"))
                type = PSN.TROPHY_GOLD;
            else if (trophyType.equals("PLATINUM"))
                type = PSN.TROPHY_PLATINUM;
            else if (trophyType.equals("UNKNOWN"))
                isSecret = true;

            if (!isSecret) {
                title = htmlDecode(m.group(2));
                if ((m = PATTERN_TROPHY_DESCRIPTION.matcher(trophyContent)).find())
                    description = htmlDecode(m.group(1));
            }

            long earned = 0;
            String earnedText = null;
            if (trophyUnlocked) {
                // No more date info
                earned = System.currentTimeMillis();
                earnedText = mContext.getString(R.string.unlocked);
            }

            ContentValues cv = new ContentValues(20);

            cv.put(Trophies.TITLE, title);
            cv.put(Trophies.DESCRIPTION, description);
            cv.put(Trophies.SORT_ORDER, index);
            cv.put(Trophies.EARNED, earned);
            cv.put(Trophies.EARNED_TEXT, earnedText);
            cv.put(Trophies.ICON_URL, iconUrl);
            cv.put(Trophies.GAME_ID, gameId);
            cv.put(Trophies.IS_SECRET, isSecret ? 1 : 0);
            cv.put(Trophies.TYPE, type);

            cvList.add(cv);
            index++;
        }

        if (App.getConfig().logToConsole())
            started = displayTimeTaken("New trophy parsing", started);

        ContentValues[] cva = new ContentValues[cvList.size()];
        cvList.toArray(cva);

        cr.delete(Trophies.CONTENT_URI, Trophies.GAME_ID + "=" + gameId, null);

        // Bulk-insert new trophies
        cr.bulkInsert(Trophies.CONTENT_URI, cva);
        cr.notifyChange(Trophies.CONTENT_URI, null);

        if (App.getConfig().logToConsole())
            started = displayTimeTaken("New trophy processing", started);

        // Update the game to remove the 'dirty' attribute
        ContentValues cv = new ContentValues(10);
        cv.put(Games.TROPHIES_DIRTY, 0);
        cr.update(Games.CONTENT_URI, cv, Games._ID + "=" + gameId, null);

        cr.notifyChange(ContentUris.withAppendedId(Games.CONTENT_URI, gameId), null);

        if (App.getConfig().logToConsole())
            displayTimeTaken("Updating Game", started);
    }

    @SuppressLint("DefaultLocale")
    @Override
    protected void parseFriends(PsnAccount account) throws ParserException, IOException {
        synchronized (PsnEuParser.class) {
            ContentResolver cr = mContext.getContentResolver();
            ContentValues cv;
            List<ContentValues> newCvs = new ArrayList<ContentValues>(100);
            final long accountId = account.getId();

            int rowsInserted = 0;
            int rowsUpdated = 0;
            int rowsDeleted = 0;

            long updated = System.currentTimeMillis();
            long started = updated;

            // Handle pending requests
            String page = getResponse(URL_FRIENDS);

            Matcher m;
            Matcher friendMatcher = PATTERN_FRIENDS_PENDING.matcher(page);

            while (friendMatcher.find()) {
                String onlineId = htmlDecode(friendMatcher.group(1));
                Cursor c = cr.query(Friends.CONTENT_URI, FRIEND_ID_PROJECTION,
                        Friends.ACCOUNT_ID + "=" + account.getId() + " AND " + Friends.ONLINE_ID + "=?",
                        new String[] { onlineId }, null);

                long friendId = -1;

                try {
                    if (c != null && c.moveToFirst())
                        friendId = c.getLong(0);
                } finally {
                    if (c != null)
                        c.close();
                }

                cv = new ContentValues(15);

                cv.put(Friends.DELETE_MARKER, updated);
                cv.put(Friends.ONLINE_STATUS, PSN.STATUS_PENDING);

                if (friendId < 0) {
                    // New
                    cv.put(Friends.ONLINE_ID, onlineId);
                    cv.put(Friends.ACCOUNT_ID, accountId);
                    cv.put(Friends.PROGRESS, 0);
                    cv.putNull(Friends.ICON_URL);
                    cv.put(Friends.LEVEL, 0);
                    cv.put(Friends.TROPHIES_PLATINUM, 0);
                    cv.put(Friends.TROPHIES_GOLD, 0);
                    cv.put(Friends.TROPHIES_SILVER, 0);
                    cv.put(Friends.TROPHIES_BRONZE, 0);
                    cv.putNull(Friends.PLAYING);
                    cv.put(Friends.LAST_UPDATED, 0);

                    newCvs.add(cv);
                } else {
                    cr.update(ContentUris.withAppendedId(Friends.CONTENT_URI, friendId), cv, null, null);

                    rowsUpdated++;
                }
            }

            // Handle rest of friends
            page = getResponse(URL_FRIENDS_AJAX);
            friendMatcher = PATTERN_FRIENDS.matcher(page);

            while (friendMatcher.find()) {
                String friendData = friendMatcher.group(1);

                String onlineId;
                if (!(m = PATTERN_FRIEND_ONLINE_ID.matcher(friendData)).find())
                    continue;

                onlineId = htmlDecode(m.group(1));

                int level = 0;
                if ((m = PATTERN_FRIEND_LEVEL.matcher(friendData)).find())
                    level = Integer.parseInt(m.group(1));

                String iconUrl = null;
                if ((m = PATTERN_FRIEND_AVATAR.matcher(friendData)).find())
                    iconUrl = getLargeAvatarIcon(resolveImageUrl(URL_FRIENDS_AJAX, m.group(1)));

                String comment = null;
                if ((m = PATTERN_FRIEND_COMMENT.matcher(friendData)).find()) {
                    comment = htmlDecode(m.group(1));
                    if (comment != null && comment.equals("null"))
                        comment = null;
                }

                int memberType = PSN.MEMBER_TYPE_FREE;
                if ((m = PATTERN_FRIEND_IS_PLUS.matcher(friendData)).find()
                        && m.group(1).equalsIgnoreCase("true")) {
                    memberType = PSN.MEMBER_TYPE_PLUS;
                }

                int bronze = 0;
                int silver = 0;
                int gold = 0;
                int platinum = 0;

                m = PATTERN_FRIEND_TROPHY.matcher(friendData);
                while (m.find()) {
                    String type = m.group(1).toLowerCase();
                    if ("bronze".equals(type))
                        bronze = Integer.parseInt(m.group(2));
                    else if ("silver".equals(type))
                        silver = Integer.parseInt(m.group(2));
                    else if ("gold".equals(type))
                        gold = Integer.parseInt(m.group(2));
                    else if ("platinum".equals(type))
                        platinum = Integer.parseInt(m.group(2));
                }

                boolean inGame = false;
                int status = PSN.STATUS_OTHER;
                if ((m = PATTERN_FRIEND_STATUS.matcher(friendData)).find()) {
                    String presence = m.group(1).toLowerCase();
                    if (presence.equals("offline"))
                        status = PSN.STATUS_OFFLINE;
                    else if (presence.equals("online"))
                        status = PSN.STATUS_ONLINE;
                    else if (presence.equals("online-ingame")) {
                        status = PSN.STATUS_ONLINE;
                        inGame = true;
                    } else if (presence.equals("online-away"))
                        status = PSN.STATUS_AWAY;
                    else if (presence.equals("online-ingame-away")) {
                        status = PSN.STATUS_AWAY;
                        inGame = true;
                    } else if (presence.equals("pending"))
                        status = PSN.STATUS_PENDING;
                }

                String playing = null;
                if ((m = PATTERN_FRIEND_PLAYING.matcher(friendData)).find()) {
                    String activity = htmlDecode(m.group(1)).trim();

                    if (activity != null && activity.length() > 0) {
                        if (inGame)
                            playing = mContext.getString(R.string.playing_f, activity);
                        else
                            playing = activity;
                    }
                }

                Cursor c = cr.query(Friends.CONTENT_URI, FRIEND_ID_PROJECTION,
                        Friends.ACCOUNT_ID + "=" + account.getId() + " AND " + Friends.ONLINE_ID + "=?",
                        new String[] { onlineId }, null);

                long friendId = -1;

                try {
                    if (c != null && c.moveToFirst())
                        friendId = c.getLong(0);
                } finally {
                    if (c != null)
                        c.close();
                }

                cv = new ContentValues(15);

                cv.put(Friends.ICON_URL, iconUrl);
                cv.put(Friends.LEVEL, level);
                cv.put(Friends.MEMBER_TYPE, memberType);
                cv.put(Friends.COMMENT, comment);
                cv.put(Friends.LEVEL, level);
                cv.put(Friends.ONLINE_STATUS, status);
                cv.put(Friends.TROPHIES_PLATINUM, platinum);
                cv.put(Friends.TROPHIES_GOLD, gold);
                cv.put(Friends.TROPHIES_SILVER, silver);
                cv.put(Friends.TROPHIES_BRONZE, bronze);
                cv.put(Friends.PLAYING, playing);
                cv.put(Friends.DELETE_MARKER, updated);

                if (friendId < 0) {
                    // New
                    cv.put(Friends.ONLINE_ID, onlineId);
                    cv.put(Friends.ACCOUNT_ID, accountId);
                    cv.put(Friends.PROGRESS, 0);
                    cv.put(Friends.LAST_UPDATED, 0);

                    newCvs.add(cv);
                } else {
                    cr.update(ContentUris.withAppendedId(Friends.CONTENT_URI, friendId), cv, null, null);

                    rowsUpdated++;
                }
            }

            // Remove friends
            rowsDeleted = cr.delete(Friends.CONTENT_URI,
                    Friends.ACCOUNT_ID + "=" + accountId + " AND " + Friends.DELETE_MARKER + "!=" + updated, null);

            if (newCvs.size() > 0) {
                ContentValues[] cvs = new ContentValues[newCvs.size()];
                newCvs.toArray(cvs);

                rowsInserted = cr.bulkInsert(Friends.CONTENT_URI, cvs);
            }

            account.refresh(Preferences.get(mContext));
            account.setLastFriendUpdate(System.currentTimeMillis());
            account.save(Preferences.get(mContext));

            cr.notifyChange(Friends.CONTENT_URI, null);

            if (App.getConfig().logToConsole())
                started = displayTimeTaken("Friend page processing [I:" + rowsInserted + ";U:" + rowsUpdated + ";D:"
                        + rowsDeleted + "]", started);
        }
    }

    @SuppressLint("DefaultLocale")
    @Override
    protected void parseFriendSummary(PsnAccount account, String friendOnlineId)
            throws ParserException, IOException {
        String url = String.format(URL_FRIEND_SUMMARY_f, URLEncoder.encode(friendOnlineId, "UTF-8"));
        String friendData = getResponse(url);

        ContentResolver cr = mContext.getContentResolver();
        Cursor c = cr.query(Friends.CONTENT_URI, FRIEND_ID_PROJECTION,
                Friends.ACCOUNT_ID + "=" + account.getId() + " AND " + Friends.ONLINE_ID + "=?",
                new String[] { friendOnlineId }, null);

        long updated = System.currentTimeMillis();
        long started = updated;
        long friendId = -1;

        try {
            if (c != null && c.moveToFirst())
                friendId = c.getLong(0);
        } finally {
            if (c != null)
                c.close();
        }

        Matcher m;
        Matcher friendMatcher = PATTERN_FRIEND_SUMMARY.matcher(friendData);
        if (friendMatcher.find() && friendMatcher.find()) // skip the first
        {
            String friendCard = friendMatcher.group(1);

            String onlineId = null;
            if ((m = PATTERN_FRIEND_SUMMARY_ONLINE_ID.matcher(friendCard)).find())
                onlineId = htmlDecode(m.group(1));

            if (onlineId != null) {
                int progress = 0;
                if ((m = PATTERN_FRIEND_SUMMARY_PROGRESS.matcher(friendCard)).find())
                    progress = Integer.parseInt(m.group(1));

                int level = 0;
                if ((m = PATTERN_FRIEND_SUMMARY_LEVEL.matcher(friendCard)).find())
                    level = Integer.parseInt(m.group(1));

                String iconUrl = null;
                if ((m = PATTERN_FRIEND_SUMMARY_AVATAR.matcher(friendCard)).find())
                    iconUrl = getLargeAvatarIcon(resolveImageUrl(url, m.group(1)));

                int memberType = PSN.MEMBER_TYPE_FREE;
                if ((m = PATTERN_FRIEND_SUMMARY_IS_PLUS.matcher(friendCard)).find())
                    memberType = PSN.MEMBER_TYPE_PLUS;

                int bronze = 0;
                int silver = 0;
                int gold = 0;
                int platinum = 0;

                m = PATTERN_FRIEND_SUMMARY_TROPHIES.matcher(friendCard);
                while (m.find()) {
                    String type = m.group(1).toLowerCase();
                    if ("bronze".equals(type))
                        bronze = Integer.parseInt(m.group(2));
                    else if ("silver".equals(type))
                        silver = Integer.parseInt(m.group(2));
                    else if ("gold".equals(type))
                        gold = Integer.parseInt(m.group(2));
                    else if ("platinum".equals(type))
                        platinum = Integer.parseInt(m.group(2));
                }

                ContentValues cv = new ContentValues(15);

                cv.put(Friends.LAST_UPDATED, updated);
                cv.put(Friends.ONLINE_ID, onlineId);
                cv.put(Friends.ICON_URL, iconUrl);
                cv.put(Friends.LEVEL, level);
                cv.put(Friends.PROGRESS, progress);
                cv.put(Friends.TROPHIES_PLATINUM, platinum);
                cv.put(Friends.TROPHIES_GOLD, gold);
                cv.put(Friends.TROPHIES_SILVER, silver);
                cv.put(Friends.TROPHIES_BRONZE, bronze);
                cv.put(Friends.MEMBER_TYPE, memberType);

                if (friendId < 0) {
                    // New
                    cv.put(Friends.ACCOUNT_ID, account.getId());
                    cv.put(Friends.ONLINE_STATUS, PSN.STATUS_OTHER);
                    cv.put(Friends.PLAYING, (String) null);

                    cr.insert(Friends.CONTENT_URI, cv);
                } else {
                    // Existing
                    cr.update(Friends.CONTENT_URI, cv, Friends._ID + "=" + friendId, null);
                }

                cr.notifyChange(Friends.CONTENT_URI, null);
            }
        }

        if (App.getConfig().logToConsole())
            started = displayTimeTaken("parseCompareGames/processing", started);
    }

    @Override
    protected GamerProfileInfo parseGamerProfile(PsnAccount account, String psnId)
            throws ParserException, IOException {
        String url = String.format(URL_FRIEND_SUMMARY_f, URLEncoder.encode(psnId, "UTF-8"));
        String friendData = getResponse(url);

        long updated = System.currentTimeMillis();
        long started = updated;

        GamerProfileInfo gpi = new GamerProfileInfo();

        Matcher m;
        Matcher friendMatcher = PATTERN_FRIEND_SUMMARY.matcher(friendData);
        if (friendMatcher.find() && friendMatcher.find()) // skip the first
        {
            String friendCard = friendMatcher.group(1);

            String onlineId = null;
            if ((m = PATTERN_FRIEND_SUMMARY_ONLINE_ID.matcher(friendCard)).find())
                onlineId = htmlDecode(m.group(1));

            if (onlineId != null) {
                int progress = 0;
                if ((m = PATTERN_FRIEND_SUMMARY_PROGRESS.matcher(friendCard)).find())
                    progress = Integer.parseInt(m.group(1));

                int level = 0;
                if ((m = PATTERN_FRIEND_SUMMARY_LEVEL.matcher(friendCard)).find())
                    level = Integer.parseInt(m.group(1));

                String iconUrl = null;
                if ((m = PATTERN_FRIEND_SUMMARY_AVATAR.matcher(friendCard)).find())
                    iconUrl = getLargeAvatarIcon(resolveImageUrl(url, m.group(1)));

                int bronze = 0;
                int silver = 0;
                int gold = 0;
                int platinum = 0;

                m = PATTERN_FRIEND_SUMMARY_TROPHIES.matcher(friendCard);
                while (m.find()) {
                    String type = m.group(1);
                    if ("bronze".equalsIgnoreCase(type))
                        bronze = Integer.parseInt(m.group(2));
                    else if ("silver".equalsIgnoreCase(type))
                        silver = Integer.parseInt(m.group(2));
                    else if ("gold".equalsIgnoreCase(type))
                        gold = Integer.parseInt(m.group(2));
                    else if ("platinum".equalsIgnoreCase(type))
                        platinum = Integer.parseInt(m.group(2));
                }

                gpi.OnlineId = onlineId;
                gpi.AvatarUrl = iconUrl;
                gpi.Level = level;
                gpi.Progress = progress;
                gpi.PlatinumTrophies = platinum;
                gpi.GoldTrophies = gold;
                gpi.SilverTrophies = silver;
                gpi.BronzeTrophies = bronze;
                gpi.OnlineStatus = PSN.STATUS_OTHER;
                gpi.Playing = null;
            }
        }

        if (App.getConfig().logToConsole())
            started = displayTimeTaken("parseGamerProfile/processing", started);

        return gpi;
    }

    @Override
    protected ComparedGameInfo parseCompareGames(PsnAccount account, String friendId)
            throws ParserException, IOException {
        // Request comparison
        String comparePage = getResponse(String.format(URL_COMPARE_GAMES_f, URLEncoder.encode(friendId, "UTF-8")));

        long started = System.currentTimeMillis();

        ComparedGameInfo cgi = new ComparedGameInfo(mContext.getContentResolver());
        cgi.myAvatarIconUrl = account.getIconUrl();

        Matcher m;

        int friendIndex = 1;

        // Determine the column index of the friend
        m = PATTERN_COMPARE_USERNAME.matcher(comparePage);
        for (int i = 0; m.find(); i++) {
            String userName = m.group(1);
            if (userName.equalsIgnoreCase(friendId)) {
                friendIndex = i;
                break;
            }
        }

        m = PATTERN_COMPARE_AVATAR_URL.matcher(comparePage);
        for (int i = 0; i < friendIndex; i++)
            m.find(); // Skip to needed column

        if (m.find())
            cgi.yourAvatarIconUrl = getLargeAvatarIcon(resolveImageUrl(URL_PROFILE_SUMMARY, m.group(1)));

        Matcher rowMatcher = PATTERN_COMPARED_GAMES.matcher(comparePage);
        while (rowMatcher.find()) {
            String uid = rowMatcher.group(1);
            String gameContent = rowMatcher.group(2);

            String title = null;
            String iconUrl = null;

            int selfPlatinum = 0;
            int selfGold = 0;
            int selfSilver = 0;
            int selfBronze = 0;
            int selfProgress = 0;
            boolean selfPlayed = false;

            int oppPlatinum = 0;
            int oppGold = 0;
            int oppSilver = 0;
            int oppBronze = 0;
            int oppProgress = 0;
            boolean oppPlayed = false;

            if ((m = PATTERN_COMPARE_GAME_TITLE.matcher(gameContent)).find())
                title = htmlDecode(m.group(1));

            if ((m = PATTERN_COMPARE_GAME_ICON.matcher(gameContent)).find())
                iconUrl = getLargeTrophyIcon(resolveImageUrl(URL_COMPARE_GAMES_f, m.group(1)));

            Matcher playerMatcher = PATTERN_COMPARE_GAME_PLAYER.matcher(gameContent);
            if (playerMatcher.find()) {
                // Skip the first (game data)

                if (playerMatcher.find()) {
                    // Account owner
                    String playerContent = playerMatcher.group(1);

                    m = PATTERN_COMPARE_GAME_PROGRESS.matcher(playerContent);
                    if (m.find()) {
                        selfPlayed = true;
                        selfProgress = Integer.parseInt(m.group(1));

                        m = PATTERN_COMPARE_GAME_TROPHIES.matcher(playerContent);
                        if (m.find()) {
                            selfBronze = Integer.parseInt(m.group(1));
                            if (m.find()) {
                                selfSilver = Integer.parseInt(m.group(1));
                                if (m.find()) {
                                    selfGold = Integer.parseInt(m.group(1));
                                    if (m.find()) {
                                        selfPlatinum = Integer.parseInt(m.group(1));
                                    }
                                }
                            }
                        }
                    }
                }

                boolean found = false;
                for (int i = 0; i < friendIndex; i++)
                    found = playerMatcher.find(); // Skip to needed column

                if (found) {
                    // Opponent
                    String oppContent = playerMatcher.group(1);

                    m = PATTERN_COMPARE_GAME_PROGRESS.matcher(oppContent);
                    if (m.find()) {
                        oppPlayed = true;
                        oppProgress = Integer.parseInt(m.group(1));

                        m = PATTERN_COMPARE_GAME_TROPHIES.matcher(oppContent);
                        if (m.find()) {
                            oppBronze = Integer.parseInt(m.group(1));
                            if (m.find()) {
                                oppSilver = Integer.parseInt(m.group(1));
                                if (m.find()) {
                                    oppGold = Integer.parseInt(m.group(1));
                                    if (m.find()) {
                                        oppPlatinum = Integer.parseInt(m.group(1));
                                    }
                                }
                            }
                        }
                    }
                }
            }

            cgi.cursor.addItem(uid, title, iconUrl, selfPlayed, selfPlatinum, selfGold, selfSilver, selfBronze,
                    selfProgress, oppPlayed, oppPlatinum, oppGold, oppSilver, oppBronze, oppProgress);
        }

        if (App.getConfig().logToConsole())
            started = displayTimeTaken("parseCompareGames/processing", started);

        return cgi;
    }

    @Override
    protected ComparedTrophyInfo parseCompareTrophies(PsnAccount account, String friendId, String gameId)
            throws ParserException, IOException {
        String url = String.format(URL_COMPARE_TROPHIES_f, URLEncoder.encode(gameId, "UTF-8"));
        String page = getResponse(url);
        long started = System.currentTimeMillis();

        ComparedTrophyInfo cti = new ComparedTrophyInfo(mContext.getContentResolver());

        Matcher m;
        Matcher rowMatcher = PATTERN_COMPARED_TROPHIES.matcher(page);
        if (!rowMatcher.find())
            return cti;

        // First row is GT's
        int friendColumn = 1;
        String row = rowMatcher.group(1);
        Matcher columnMatcher = PATTERN_COMPARED_TROPHY_COLUMN.matcher(row);

        for (int i = 0; columnMatcher.find(); i++) {
            String column = columnMatcher.group(3);
            if ((m = PATTERN_COMPARED_FRIEND_ID.matcher(column)).find())
                if (m.group(1).equalsIgnoreCase(friendId))
                    friendColumn = i;
        }

        // Consume the next 2
        for (int i = 0; i < 2; i++)
            if (!rowMatcher.find())
                return cti;

        while (rowMatcher.find()) {
            row = rowMatcher.group(1);

            String iconUrl = null;
            int type = 0;
            String title = null;
            String description = null;
            boolean isSecret = false;
            boolean isLocked = true;
            String selfEarned = null;
            String oppEarned = null;

            if ((m = PATTERN_COMPARED_TROPHY_ICON.matcher(row)).find())
                iconUrl = getLargeTrophyIcon(resolveImageUrl(url, m.group(1)));
            if ((m = PATTERN_COMPARED_TROPHY_TITLE.matcher(row)).find()) {
                title = htmlDecode(m.group(2));
                String trophyType = m.group(1);
                if ("bronze".equalsIgnoreCase(trophyType))
                    type = PSN.TROPHY_BRONZE;
                else if ("silver".equalsIgnoreCase(trophyType))
                    type = PSN.TROPHY_SILVER;
                else if ("gold".equalsIgnoreCase(trophyType))
                    type = PSN.TROPHY_GOLD;
                else if ("platinum".equalsIgnoreCase(trophyType))
                    type = PSN.TROPHY_PLATINUM;
                else if ("unknown".equalsIgnoreCase(trophyType))
                    type = PSN.TROPHY_SECRET;
            }

            if ((m = PATTERN_COMPARED_TROPHY_DESCRIPTION.matcher(row)).find())
                description = htmlDecode(m.group(1));

            columnMatcher = PATTERN_COMPARED_TROPHY_COLUMN.matcher(row);
            for (int i = 0; columnMatcher.find(); i++) {
                if (i == 0) {
                    // General information
                    isLocked = "trophy-unlocked-false".equalsIgnoreCase(columnMatcher.group(2));
                    selfEarned = isLocked ? mContext.getString(R.string.locked)
                            : mContext.getString(R.string.unlocked);
                } else if (i == friendColumn) {
                    // Opponent
                    String column = columnMatcher.group(3);
                    boolean isLockedForOpp = PATTERN_COMPARED_TROPHY_IS_LOCKED.matcher(column).find();
                    oppEarned = isLockedForOpp ? mContext.getString(R.string.locked)
                            : mContext.getString(R.string.unlocked);
                }
            }

            cti.cursor.addItem(title, description, iconUrl, type, isSecret, isLocked, selfEarned, oppEarned);
        }

        if (App.getConfig().logToConsole())
            started = displayTimeTaken("parseCompareTrophies/processing", started);

        return cti;
    }

    @SuppressLint("SimpleDateFormat")
    @Override
    protected GameCatalogList parseGameCatalog(int console, int page, int releaseStatus, int sortOrder)
            throws ParserException, IOException {
        String[] rating = null;
        String[] genre = null;
        String releaseSpec = null;
        String sortOrderSpec = null;
        String consoleSpec = null;

        if (console == PSN.CATALOG_CONSOLE_PSVITA)
            consoleSpec = "psvita";
        else if (console == PSN.CATALOG_CONSOLE_PSP)
            consoleSpec = "psp";
        else if (console == PSN.CATALOG_CONSOLE_PS2)
            consoleSpec = "ps2";
        else
            consoleSpec = "ps3";

        if (sortOrder == PSN.CATALOG_SORT_BY_ALPHA)
            sortOrderSpec = "ALPHANUMERIC";
        else
            sortOrderSpec = "RELEASE_DATE";

        if (releaseStatus == PSN.CATALOG_RELEASE_OUT_NOW)
            releaseSpec = "out-now";
        else
            releaseSpec = "coming-soon";

        int pageSize = 12;

        List<NameValuePair> inputs = new ArrayList<NameValuePair>(3);

        addValue(inputs, "sort", sortOrderSpec); // ?
        addValue(inputs, "vertical", consoleSpec); // ?
        addValue(inputs, "gameFilter.tab", releaseSpec);
        addValue(inputs, "gameFilter.ageRatingBandString", (rating == null) ? "18" : rating);

        if (genre != null)
            addValue(inputs, "gameFilter.genreIdsString", genre);

        addValue(inputs, "page", page + "");

        long started = System.currentTimeMillis();

        String catalogPage = getResponse(URL_GAME_CATALOG, inputs, true);

        if (App.getConfig().logToConsole())
            started = displayTimeTaken("parseGameCatalog/data fetch", started);

        int fetchedPage = page;
        int totalPages = 0;

        Matcher m;
        if ((m = PATTERN_GAME_CATALOG_STATS.matcher(catalogPage)).find()) {
            fetchedPage = Integer.parseInt(m.group(1));
            totalPages = Integer.parseInt(m.group(2));
        }

        Matcher itemMatcher = PATTERN_GAME_CATALOG_ITEMS.matcher(catalogPage);

        GameCatalogList catalog = new GameCatalogList();

        catalog.PageNumber = fetchedPage;
        catalog.PageSize = pageSize;
        catalog.MorePages = totalPages > fetchedPage;

        if (App.getConfig().logToConsole())
            App.logv("fetched: " + fetchedPage + "; totalPages: " + totalPages + "; more?: " + catalog.MorePages);

        URL baseUrl = new URL(URL_GAME_CATALOG);

        SimpleDateFormat mdyParser = new SimpleDateFormat("d MMM yyyy");
        Pattern mdyDetector = Pattern.compile("^\\d+ \\S+ \\d{4}$");
        Pattern myDetector = Pattern.compile("^\\S+ \\d{4}$");

        while (itemMatcher.find()) {
            String content = itemMatcher.group(1);
            m = PATTERN_GAME_CATALOG_NODE.matcher(content);

            GameCatalogItem item = new GameCatalogItem();

            while (m.find()) {
                String nodeName = m.group(1);

                if (nodeName != null) {
                    if (nodeName.equals("title")) {
                        if (item.Title == null) // LAME!
                            item.Title = htmlDecode(m.group(2));
                    } else if (nodeName.equals("release_date")) {
                        item.ReleaseDate = htmlDecode(m.group(2));
                        item.ReleaseDateTicks = 0;

                        String parseDate = null;
                        if (mdyDetector.matcher(item.ReleaseDate).find())
                            parseDate = item.ReleaseDate;
                        else if (myDetector.matcher(item.ReleaseDate).find())
                            parseDate = "1 " + item.ReleaseDate;

                        if (parseDate != null) {
                            try {
                                item.ReleaseDateTicks = mdyParser.parse(parseDate).getTime();
                            } catch (Exception e) {
                            }
                        }
                    } else if (nodeName.equals("genre"))
                        item.Genre = htmlDecode(m.group(2));
                    else if (nodeName.equals("detail_link"))
                        item.DetailUrl = new URL(baseUrl, m.group(2)).toString();
                    else if (nodeName.equals("thumbnail_image_url"))
                        item.BoxartUrl = new URL(baseUrl, m.group(2)).toString();
                }
            }

            if (item.Title == null)
                continue;

            catalog.Items.add(item);
        }

        if (App.getConfig().logToConsole())
            displayTimeTaken("parseGameCatalog/parsing", started);

        return catalog;
    }

    protected GameCatalogItemDetails parseGameCatalogItemDetails(GameCatalogItem item)
            throws ParserException, IOException {
        if (item == null)
            return null;

        //boolean isAlt = false;
        GameCatalogItemDetails details = GameCatalogItemDetails.fromItem(item);

        long started = System.currentTimeMillis();

        String detailPage = getResponse(item.DetailUrl);

        if (App.getConfig().logToConsole())
            started = displayTimeTaken("parseGameCatalogItemDetails/data fetch", started);

        Matcher m;
        if ((m = PATTERN_GAME_CATALOG_DETAIL_DESC.matcher(detailPage)).find()) {
            details.Description = m.group(1);
        } else if ((m = PATTERN_GAME_CATALOG_DETAIL_DESC_ALT.matcher(detailPage)).find()) {
            details.Description = m.group(1);
            //isAlt = true;
        }

        if (App.getConfig().logToConsole())
            displayTimeTaken("parseGameCatalogItemDetails/parsing", started);

        return details;
    }

    private String getLargeTrophyIcon(String url) {
        if (url == null)
            return null;

        int pos = url.indexOf("/trophy/");
        if (pos < 1)
            return url;

        return LARGE_TROPHY_ICON_PREFIX + url.substring(pos).trim();
    }

    private String getLargeAvatarIcon(String url) {
        if (url == null)
            return null;

        int pos = url.indexOf("/avatar/");
        if (pos < 1)
            return url;

        return LARGE_AVATAR_ICON_PREFIX + url.substring(pos).trim();
    }

    @Override
    public RssChannel fetchBlog() throws ParserException {
        try {
            return RssHandler.getFeed(URL_BLOG);
        } catch (Exception e) {
            throw new ParserException(mContext, R.string.error_loading_blog);
        }
    }
}