package com.ioabsoftware.gameraven.networking; import android.app.AlertDialog; import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.view.ViewGroup.LayoutParams; import android.webkit.WebView; import android.widget.EditText; import android.widget.LinearLayout; import com.ioabsoftware.gameraven.AllInOneV2; import com.ioabsoftware.gameraven.BuildConfig; import com.ioabsoftware.gameraven.db.History; import com.ioabsoftware.gameraven.db.HistoryDBAdapter; import com.ioabsoftware.gameraven.util.DocumentParser; import com.ioabsoftware.gameraven.util.FinalDoc; import com.ioabsoftware.gameraven.util.Theming; import com.koushikdutta.async.future.Future; import com.koushikdutta.async.future.FutureCallback; import com.koushikdutta.async.http.ConnectionClosedException; import com.koushikdutta.ion.Ion; import com.koushikdutta.ion.Response; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.TextNode; import org.jsoup.select.Elements; import java.net.UnknownHostException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.TimeoutException; import de.keyboardsurfer.android.widget.crouton.Crouton; /** * Session is used to establish and maintain GFAQs sessions, and to send GET and POST requests. * * @author Charles Rosaaen, Insanity On A Bun Software */ public class Session implements FutureCallback<Response<FinalDoc>> { /** * The root of GFAQs. */ public static final String ROOT = "https://www.gamefaqs.com"; private String lastAttemptedPath = "not set"; public String getLastAttemptedPath() { return lastAttemptedPath; } private NetDesc lastAttemptedDesc = NetDesc.UNSPECIFIED; public NetDesc getLastAttemptedDesc() { return lastAttemptedDesc; } /** * The latest page, with excess get data. */ private String lastPath = null; /** * Get's the path of the latest page. */ public String getLastPath() { return lastPath; } /** * Get's the path of the latest page, stripped of any GET data. */ public String getLastPathWithoutData() { if (lastPath.contains("?")) return lastPath.substring(0, lastPath.indexOf('?')); else return lastPath; } /** * The latest Response body as an array of bytes. */ private byte[] lastResBodyAsBytes = null; /** * The latest description. */ private NetDesc lastDesc = null; /** * Get's the description of the latest page. */ public NetDesc getLastDesc() { return lastDesc; } /** * The name of the user for this session. */ private static String user = null; /** * Get's the name of the session user. */ public static String getUser() { return user; } public static boolean isLoggedIn() { return user != null; } private static int userLevel = 0; public static boolean userCanDeleteClose() { return userLevel > 13; } public static boolean userCanViewAMP() { return userLevel > 14; } public static boolean userCanMarkMsgs() { return userLevel > 19; } public static boolean userCanEditMsgs() { return userLevel > 19; } /** * Quickpost and create poll topics */ public static boolean userHasAdvancedPosting() { return userLevel > 29; } public static boolean applySavedScroll; public static int[] savedScrollVal; /** * The password of the user for this session. */ private String password = null; private String sessionKey; public String getSessionKey() {return sessionKey;} /** * The current activity. */ private AllInOneV2 aio; private boolean addToHistory = true; public void forceNoHistoryAddition() { if (BuildConfig.DEBUG) AllInOneV2.wtl("forcing history addition off"); addToHistory = false; } private HistoryDBAdapter hAdapter; public static String RESUME_INIT_URL = "RESUME-SESSION"; private String initUrl = null; private NetDesc initDesc = null; /********************************************** * START METHODS **********************************************/ /** * Create a new session with no user logged in * that starts at the homepage. */ public Session(AllInOneV2 aioIn) { this(aioIn, null, null); } /** * Construct a new session for the specified user, * using the specified password, that finishes on * the GFAQs homepage. * * @param userIn The user for this session. * @param passwordIn The password for this session. */ public Session(AllInOneV2 aioIn, String userIn, String passwordIn) { this(aioIn, userIn, passwordIn, null, null); } /** * Construct a new session for the specified user, * using the specified password, that finishes on * initUrlIn using initDescIn. Passing null for * initUrlIn will finish on the GFAQs homepage. * * @param userIn The user for this session. * @param passwordIn The password for this session. * @param initUrlIn The URL to load once successfully logged in. * @param initDescIn The desc to use once successfully logged in. */ public Session(AllInOneV2 aioIn, String userIn, String passwordIn, String initUrlIn, NetDesc initDescIn) { initUrl = initUrlIn; initDesc = initDescIn; finalConstructor(aioIn, userIn, passwordIn); } /** * Final construction method. * * @param aioIn The current activity * @param userIn Username, or null if no user * @param passwordIn Password, or null if no user */ private void finalConstructor(AllInOneV2 aioIn, String userIn, String passwordIn) { aio = aioIn; if (BuildConfig.DEBUG) AllInOneV2.wtl("NEW SESSION"); aio.navDrawerReset(); netManager = (ConnectivityManager) aio.getSystemService(Context.CONNECTIVITY_SERVICE); hAdapter = new HistoryDBAdapter(); openHistoryDB(); if (initUrl == null || !initUrl.equals(RESUME_INIT_URL)) hAdapter.clearTable(); user = userIn; password = passwordIn; // reset the Session unread PM and TT counters AllInOneV2.getSettingsPref().edit().putInt("unreadPMCount", 0).apply(); AllInOneV2.getSettingsPref().edit().putInt("unreadTTCount", 0).apply(); // clear out cookies Ion.getDefault(aio).getCookieMiddleware().clear(); if (user == null) { if (BuildConfig.DEBUG) AllInOneV2.wtl("session constructor, user is null, starting logged out session"); get(NetDesc.BOARD_JUMPER, ROOT + "/boards/"); aio.setLoginName(user); } else { if (BuildConfig.DEBUG) AllInOneV2.wtl("session constructor, user is not null, starting logged in session"); get(NetDesc.LOGIN_S1, ROOT + "/boards/"); aio.setLoginName(user); aio.showLoggingInDialog(user); } } /** * Builds a URL based on path. * * @param path The path to build a URL off of. Can * be relative or absolute. If relative, can start * with a forward slash or not. * @return The correct absolute URL for the specified * path. */ public static String buildURL(String path, NetDesc desc) { if (!path.contains("www.gamefaqs.com") && path.contains("gamefaqs.com")) path = path.replace("gamefaqs.com", "www.gamefaqs.com"); if (path.contains("http://www.gamefaqs.com")) { path = path.replace("http://www.gamefaqs.com", "https://www.gamefaqs.com"); } if (desc == NetDesc.BOARD && path.matches(".*\\d$")) { path += "-"; } // path is absolute, return it if (path.startsWith("http")) return path; // add a forward slash to path if needed if (!path.startsWith("/")) path = '/' + path; // return absolute path return ROOT + path; } private ConnectivityManager netManager; public boolean hasNetworkConnection() { NetworkInfo netInfo = netManager.getActiveNetworkInfo(); return netInfo != null && netInfo.isConnected(); } private Future currentNetworkTask; /** * Sends a GET request to a specified page. * * @param desc Description of this request, to properly handle the response later. * @param path The path to send the request to. */ public void get(NetDesc desc, String path) { if (hasNetworkConnection()) { if (desc != NetDesc.MODHIST && desc != NetDesc.FRIENDS && desc != NetDesc.FOLLOWERS && desc != NetDesc.FOLLOWING) { if (currentNetworkTask != null && !currentNetworkTask.isDone()) currentNetworkTask.cancel(true); lastAttemptedPath = path; lastAttemptedDesc = desc; preExecuteSetup(desc); currentNetworkTask = Ion.with(aio) .load("GET", buildURL(path, desc)) .as(new DocumentParser()) .withResponse() .setCallback(this); } else if (desc == NetDesc.MODHIST) aio.genError("Page Unsupported", "The moderation history page is currently unsupported in-app. Sorry.", "Ok"); else if (desc == NetDesc.FRIENDS || desc == NetDesc.FOLLOWERS || desc == NetDesc.FOLLOWING) aio.genError("Page Unsupported", "The friends and followers system is currently unsupported in-app. Sorry.", "Ok"); } else aio.noNetworkConnection(); } /** * Sends a POST request to a specified page. * * @param desc Description of this request, to properly handle the response later. * @param path The path to send the request to. * @param data The extra data to send along. */ public void post(NetDesc desc, String path, Map<String, List<String>> data) { if (hasNetworkConnection()) { if (desc != NetDesc.MODHIST) { if (currentNetworkTask != null && !currentNetworkTask.isDone()) currentNetworkTask.cancel(true); preExecuteSetup(desc); currentNetworkTask = Ion.with(aio) .load("POST", buildURL(path, desc)) .setBodyParameters(data) .as(new DocumentParser()) .withResponse() .setCallback(this); } else aio.genError("Page Unsupported", "The moderation history page is currently unsupported in-app. Sorry.", "Ok"); } else aio.noNetworkConnection(); } private NetDesc currentDesc; /** * onCompleted is called by the Future with the result or exception of the asynchronous operation. * * @param e Exception encountered by the operation * @param result Result returned from the operation */ @Override public void onCompleted(Exception e, Response<FinalDoc> result) { if (e != null && e instanceof CancellationException) return; NetDesc thisDesc = currentDesc; handleNetworkResult(e, thisDesc, result); postExecuteCleanup(thisDesc); } private void preExecuteSetup(NetDesc desc) { currentDesc = desc; switch (desc) { case AMP_LIST: case TRACKED_TOPICS: case BOARD: case BOARD_JUMPER: case TOPIC: case GAME_SEARCH: case BOARD_LIST: case MESSAGE_DETAIL: case USER_DETAIL: case USER_TAG: case MODHIST: case PM_INBOX: case PM_INBOX_DETAIL: case PM_OUTBOX: case PM_OUTBOX_DETAIL: case MSG_MARK: case TOPIC_CLOSE: case MSG_DELETE: case LOGIN_S1: case EDIT_MSG: case MSG_POST_S1: case TOPIC_POST_S1: case NOTIFS_PAGE: case NOTIFS_CLEAR: case MENTIONS_PAGE: case UNSPECIFIED: aio.preExecuteSetup(desc); break; case LOGIN_S2: case MSG_POST_S3: case TOPIC_POST_S3: case VERIFY_ACCOUNT_S1: case VERIFY_ACCOUNT_S2: case PM_SEND_S1: case PM_SEND_S2: break; } } private void handleNetworkResult(Exception e, NetDesc desc, Response<FinalDoc> result) { if (BuildConfig.DEBUG) AllInOneV2.wtl("session hNR fired, desc: " + desc.name()); try { if (e != null) throw e; if (result != null && result.getResult() != null && result.getResult().doc != null) { if (lastDesc == NetDesc.LOGIN_S2) aio.dismissLoginDialog(); result.getResult().doc.outputSettings().syntax(Document.OutputSettings.Syntax.xml); if (BuildConfig.DEBUG) AllInOneV2.wtl("parsing res"); Document doc = result.getResult().doc; String resUrl = result.getRequest().getUri().toString(); if (BuildConfig.DEBUG) AllInOneV2.wtl("resUrl: " + resUrl); if (BuildConfig.DEBUG) AllInOneV2.wtl("checking if res does not start with root"); if (!resUrl.startsWith(ROOT)) { AlertDialog.Builder b = new AlertDialog.Builder(aio); b.setTitle("Redirected"); b.setMessage("The request was redirected somewhere away from GameFAQs. " + "This usually happens if you're connected to a network that requires a login, " + "such as a paid-for wifi service. Click below to open the page in your browser.\n" + "\n" + "Redirect: " + resUrl); final String path = resUrl; b.setPositiveButton("Open Page In Browser", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(path)); aio.startActivity(browserIntent); aio.finish(); } }); b.create().show(); return; } if (BuildConfig.DEBUG) AllInOneV2.wtl("checking if pRes contains captcha"); if (!doc.select("header.page_header:contains(CAPTCHA)").isEmpty()) { String captcha = doc.select("iframe").outerHtml(); final String key = doc.getElementsByAttributeValue("name", "key").attr("value"); AlertDialog.Builder b = new AlertDialog.Builder(aio); b.setTitle("CAPTCHA Required"); LinearLayout wrapper = new LinearLayout(aio); wrapper.setOrientation(LinearLayout.VERTICAL); WebView web = new WebView(aio); web.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); web.loadDataWithBaseURL(resUrl, "<p>There have been multiple unsuccessful login attempts!</p>" + captcha, "text/html", null, null); wrapper.addView(web); final EditText form = new EditText(aio); form.setHint("Enter confirmation code (NOT CAPTCHA!!!) here"); wrapper.addView(form); b.setView(wrapper); b.setPositiveButton("Login", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { HashMap<String, List<String>> loginData = new HashMap<>(); // "EMAILADDR", user, "PASSWORD", password, "path", lastPath, "key", key loginData.put("EMAILADDR", Collections.singletonList(user)); loginData.put("PASSWORD", Collections.singletonList(password)); loginData.put("path", Collections.singletonList(ROOT)); loginData.put("key", Collections.singletonList(key)); loginData.put("recaptcha_challenge_field", Collections.singletonList(form.getText().toString())); loginData.put("recaptcha_response_field", Collections.singletonList("manual_challenge")); post(NetDesc.LOGIN_S2, "/user/login_captcha.html", loginData); } }); b.create().show(); return; } if (BuildConfig.DEBUG) AllInOneV2.wtl("checking for non-200 http response code"); int responseCode = result.getHeaders().code(); if (BuildConfig.DEBUG && responseCode != 200) Crouton.showText(aio, "HTTP Response Code: " + responseCode, Theming.croutonStyle()); if (responseCode != 200) { if (responseCode == 404) { if (BuildConfig.DEBUG) AllInOneV2.wtl("status code 404"); Elements paragraphs = doc.getElementsByTag("p"); aio.genError("404 Error", paragraphs.get(1).text() + "\n\n" + paragraphs.get(2).text(), "Ok"); return; } else if (responseCode == 403) { if (BuildConfig.DEBUG) AllInOneV2.wtl("status code 403"); Elements paragraphs = doc.getElementsByTag("p"); aio.genError("403 Error", paragraphs.get(1).text() + "\n\n" + paragraphs.get(2).text(), "Ok"); return; } else if (responseCode == 401) { if (BuildConfig.DEBUG) AllInOneV2.wtl("status code 401"); if (lastDesc == NetDesc.LOGIN_S2) { forceSkipAIOCleanup(); get(NetDesc.BOARD_JUMPER, "/boards"); } else { Elements paragraphs = doc.getElementsByTag("p"); aio.genError("401 Error", paragraphs.get(1).text() + "\n\n" + paragraphs.get(2).text(), "Ok"); } return; } } if (BuildConfig.DEBUG) AllInOneV2.wtl("checking for 408, 503, GameFAQs is Down pages"); Element firstHeader = doc.getElementsByTag("h1").first(); if (firstHeader != null && firstHeader.text().equals("408 Request Time-out")) { if (BuildConfig.DEBUG) AllInOneV2.wtl("status code 408"); aio.genError("408 Error", "Your browser didn't send a complete request in time.", "Ok"); return; } if (doc.title().equals("GameFAQs - 503 - Temporarily Unavailable")) { aio.genError("503 Error", "GameFAQs is experiencing some temporary difficulties with " + "the site. Please wait a few seconds before refreshing this page to try again.", "Ok"); return; } else if (doc.title().equals("GameFAQs is Down")) { aio.genError("GameFAQs is Down", "GameFAQs is experiencing an outage at the moment - " + "the servers are overloaded and unable to serve pages. Hopefully, this is a " + "temporary problem, and will be rectified by the time you refresh this page.", "Ok"); return; } if (BuildConfig.DEBUG) AllInOneV2.wtl("checking for suspended, banned, and new accounts, " + "as well as register.html?miss=1 page"); if (resUrl.contains("account_suspended.html")) { aio.genError("Account Suspended", "Your account seems to be suspended. Please " + "log in to your account in a web browser for more details.", "Ok"); return; } else if (resUrl.contains("account_banned.html")) { aio.genError("Account Banned", "Your account seems to be banned. Please " + "log in to your account in a web browser for more details.", "Ok"); return; } else if (resUrl.contains("welcome.php")) { aio.genError("New Account", "It looks like this is a new account. Welcome to GameFAQs! " + "There are some ground rules you'll have to go over first before you can get " + "access to the message boards. Please log in to your account in a web browser " + "and access the message boards there to view and accept the site terms and rules.", "Ok"); return; } else if (resUrl.contains("register.html?miss=1")) { aio.genError("Login Required", "You've just tried to access a feature that requires a " + "GameFAQs account. You can manage your accounts and log in through the navigation " + "drawer. If you are currently logged into an account, try removing the account " + "from the app and re-adding it.", "Ok"); return; } updateUserLevel(doc); switch (desc) { case AMP_LIST: case TRACKED_TOPICS: case BOARD: case BOARD_JUMPER: case TOPIC: case GAME_SEARCH: case BOARD_LIST: case MESSAGE_DETAIL: case USER_DETAIL: case MODHIST: case PM_INBOX: case PM_INBOX_DETAIL: case PM_OUTBOX: case PM_OUTBOX_DETAIL: case UNSPECIFIED: case LOGIN_S1: case LOGIN_S2: case MSG_DELETE: case EDIT_MSG: case MSG_POST_S1: case MSG_POST_S3: case TOPIC_POST_S1: case TOPIC_POST_S3: case VERIFY_ACCOUNT_S1: case VERIFY_ACCOUNT_S2: case NOTIFS_PAGE: case MENTIONS_PAGE: if (BuildConfig.DEBUG) AllInOneV2.wtl("addToHistory unchanged: " + addToHistory); break; case USER_TAG: case MSG_MARK: case TOPIC_CLOSE: case PM_SEND_S1: case PM_SEND_S2: case NOTIFS_CLEAR: if (BuildConfig.DEBUG) AllInOneV2.wtl("setting addToHistory to false based on current NetDesc"); addToHistory = false; break; } if (addToHistory) { addHistory(); } switch (desc) { case AMP_LIST: case TRACKED_TOPICS: case BOARD: case BOARD_JUMPER: case TOPIC: case GAME_SEARCH: case BOARD_LIST: case MESSAGE_DETAIL: case USER_DETAIL: case MODHIST: case PM_INBOX: case PM_INBOX_DETAIL: case PM_OUTBOX: case PM_OUTBOX_DETAIL: case MSG_DELETE: case UNSPECIFIED: case LOGIN_S1: case LOGIN_S2: case EDIT_MSG: case MSG_POST_S1: case MSG_POST_S3: case TOPIC_POST_S1: case TOPIC_POST_S3: case VERIFY_ACCOUNT_S1: case VERIFY_ACCOUNT_S2: case NOTIFS_PAGE: case MENTIONS_PAGE: if (BuildConfig.DEBUG) AllInOneV2.wtl("beginning lastDesc, lastRes, etc. setting"); lastDesc = desc; lastResBodyAsBytes = result.getResult().bytes; lastPath = resUrl; // replace boardaction part of url, don't want it being added to history if (lastPath.contains("/boardaction/")) lastPath = lastPath.replace("/boardaction/", "/boards/"); if (BuildConfig.DEBUG) AllInOneV2.wtl("finishing lastDesc, lastRes, etc. setting"); break; case USER_TAG: case MSG_MARK: case TOPIC_CLOSE: case PM_SEND_S1: case PM_SEND_S2: case NOTIFS_CLEAR: if (BuildConfig.DEBUG) AllInOneV2.wtl("not setting lastDesc, lastRes, etc."); break; } // reset history flag addToHistory = true; Element keyElem = doc.getElementsByAttributeValue("name", "key").first(); if (keyElem != null) sessionKey = doc.getElementsByAttributeValue("name", "key").first().attr("value"); switch (desc) { case LOGIN_S1: if (BuildConfig.DEBUG) AllInOneV2.wtl("session hNR determined this is login step 1"); String loginKey = doc.getElementsByAttributeValue("name", "key").attr("value"); HashMap<String, List<String>> loginData = new HashMap<>(); // "EMAILADDR", user, "PASSWORD", password, "path", lastPath, "key", key loginData.put("EMAILADDR", Collections.singletonList(user)); loginData.put("PASSWORD", Collections.singletonList(password)); loginData.put("path", Collections.singletonList(buildURL("answers", NetDesc.UNSPECIFIED))); loginData.put("key", Collections.singletonList(loginKey)); if (BuildConfig.DEBUG) AllInOneV2.wtl("finishing login step 1, sending step 2"); post(NetDesc.LOGIN_S2, "/user/login", loginData); break; case LOGIN_S2: if (BuildConfig.DEBUG) AllInOneV2.wtl("session hNR determined this is login step 2"); aio.setAMPLinkVisible(userCanViewAMP()); if (initUrl != null) { if (BuildConfig.DEBUG) AllInOneV2.wtl("loading previous page"); if (initUrl.equals(RESUME_INIT_URL) && canGoBack()) { aio.dismissLoginDialog(); goBack(true); aio.setNavDrawerVisibility(isLoggedIn()); } else get(initDesc, initUrl); } else if (userCanViewAMP() && AllInOneV2.getSettingsPref().getBoolean("startAtAMP", false)) { if (BuildConfig.DEBUG) AllInOneV2.wtl("loading AMP"); get(NetDesc.AMP_LIST, AllInOneV2.buildAMPLink()); } else { if (BuildConfig.DEBUG) AllInOneV2.wtl("loading board jumper"); get(NetDesc.BOARD_JUMPER, "/boards"); } break; case MSG_POST_S1: case EDIT_MSG: if (BuildConfig.DEBUG) AllInOneV2.wtl("session hNR determined this is post message step 1"); HashMap<String, List<String>> msg1Data = new HashMap<>(); msg1Data.put("messagetext", Collections.singletonList(aio.getSavedPostBody())); msg1Data.put("key", Collections.singletonList(sessionKey)); msg1Data.put("post", Collections.singletonList("Post Message")); if (!AllInOneV2.getSettingsPref().getBoolean("useGFAQsSig" + user, false)) msg1Data.put("custom_sig", Collections.singletonList(aio.getSig())); post(NetDesc.MSG_POST_S3, lastPath, msg1Data); break; case MSG_POST_S3: if (BuildConfig.DEBUG) AllInOneV2.wtl("session hNR determined this is post message step 3 (if jumping from 1 to 3, then app is quick posting)"); Elements msg3AutoFlag = doc.select("b:contains(There are one or more potential issues with your message)"); Elements msg3Error = doc.select("b:contains(There was an error posting your message)"); if (!msg3Error.isEmpty()) { if (BuildConfig.DEBUG) AllInOneV2.wtl("there was an error in post msg step 3, ending early"); aio.postError(((TextNode) msg3Error.first().nextSibling().nextSibling()).text()); postErrorDetected = true; } else if (!msg3AutoFlag.isEmpty()) { if (BuildConfig.DEBUG) AllInOneV2.wtl("autoflag got tripped in post msg step 3, getting data and showing autoflag dialog"); String msg = ((TextNode) msg3AutoFlag.first().nextSibling().nextSibling()).text(); HashMap<String, List<String>> msg3Data = new HashMap<>(); msg3Data.put("messagetext", Collections.singletonList(aio.getSavedPostBody())); msg3Data.put("post", Collections.singletonList("Post Message")); msg3Data.put("key", Collections.singletonList(sessionKey)); msg3Data.put("override", Collections.singletonList("checked")); if (!AllInOneV2.getSettingsPref().getBoolean("useGFAQsSig" + user, false)) msg3Data.put("custom_sig", Collections.singletonList(aio.getSig())); showAutoFlagWarning(lastPath, msg3Data, NetDesc.MSG_POST_S3, msg); postErrorDetected = true; } else { if (BuildConfig.DEBUG) AllInOneV2.wtl("finishing post message step 3, refreshing topic"); lastDesc = NetDesc.TOPIC; aio.enableGoToUrlDefinedPost(); Crouton.showText(aio, "Message posted.", Theming.croutonStyle()); processTopicsAndMessages(doc, resUrl, NetDesc.TOPIC); } break; case TOPIC_POST_S1: if (BuildConfig.DEBUG) AllInOneV2.wtl("session hNR determined this is post topic step 1"); HashMap<String, List<String>> tpc1Data = new HashMap<>(); tpc1Data.put("topictitle", Collections.singletonList(aio.getSavedPostTitle())); tpc1Data.put("messagetext", Collections.singletonList(aio.getSavedPostBody())); tpc1Data.put("key", Collections.singletonList(sessionKey)); tpc1Data.put("post", Collections.singletonList("Post Message")); if (!AllInOneV2.getSettingsPref().getBoolean("useGFAQsSig" + user, false)) tpc1Data.put("custom_sig", Collections.singletonList(aio.getSig())); if (aio.isUsingPoll()) { tpc1Data.put("poll_text", Collections.singletonList(aio.getPollTitle())); for (int x = 0; x < 10; x++) { if (aio.getPollOptions()[x].length() != 0) tpc1Data.put("poll_option_" + (x + 1), Collections.singletonList(aio.getPollOptions()[x])); else x = 11; } tpc1Data.put("min_level", Collections.singletonList(aio.getPollMinLevel())); } post(NetDesc.TOPIC_POST_S3, lastPath, tpc1Data); break; case TOPIC_POST_S3: if (BuildConfig.DEBUG) AllInOneV2.wtl("session hNR determined this is post topic step 3 (if jumping from 1 to 3, then app is quick posting)"); Elements tpc3AutoFlag = doc.select("b:contains(There are one or more potential issues with your message)"); Elements tpc3Error = doc.select("b:contains(There was an error posting your message)"); if (!tpc3Error.isEmpty()) { if (BuildConfig.DEBUG) AllInOneV2.wtl("there was an error in post topic step 3, ending early"); aio.postError(((TextNode) tpc3Error.first().nextSibling().nextSibling()).text()); postErrorDetected = true; } else if (!tpc3AutoFlag.isEmpty()) { if (BuildConfig.DEBUG) AllInOneV2.wtl("autoflag got tripped in post msg step 3, getting data and showing autoflag dialog"); String msg = ((TextNode) tpc3AutoFlag.first().nextSibling().nextSibling()).text(); HashMap<String, List<String>> tpc3Data = new HashMap<>(); tpc3Data.put("topictitle", Collections.singletonList(aio.getSavedPostTitle())); tpc3Data.put("messagetext", Collections.singletonList(aio.getSavedPostBody())); tpc3Data.put("post", Collections.singletonList("Post Message")); tpc3Data.put("key", Collections.singletonList(sessionKey)); tpc3Data.put("override", Collections.singletonList("checked")); if (!AllInOneV2.getSettingsPref().getBoolean("useGFAQsSig" + user, false)) tpc3Data.put("custom_sig", Collections.singletonList(aio.getSig())); showAutoFlagWarning(lastPath, tpc3Data, NetDesc.TOPIC_POST_S3, msg); postErrorDetected = true; } else { if (BuildConfig.DEBUG) AllInOneV2.wtl("finishing post topic step 3, processing new topic"); lastDesc = NetDesc.TOPIC; Crouton.showText(aio, "Topic posted.", Theming.croutonStyle()); processTopicsAndMessages(doc, resUrl, NetDesc.TOPIC); } break; case MSG_MARK: String response = doc.text(); int start = response.indexOf("\":\"") + 3; int end = response.indexOf("\",\""); String markMessage = response.substring(start, end); Crouton.showText(aio, markMessage, Theming.croutonStyle()); break; case MSG_DELETE: Crouton.showText(aio, "Message deleted.", Theming.croutonStyle()); applySavedScroll = true; savedScrollVal = aio.getScrollerVertLoc(); lastDesc = NetDesc.TOPIC; processTopicsAndMessages(doc, resUrl, NetDesc.TOPIC); break; case TOPIC_CLOSE: Crouton.showText(aio, "Topic closed successfully.", Theming.croutonStyle()); goBack(true); break; case USER_TAG: case NOTIFS_CLEAR: refresh(); break; case PM_SEND_S1: HashMap<String, List<String>> pmData = new HashMap<>(); pmData.put("key", Collections.singletonList(sessionKey)); pmData.put("to", Collections.singletonList(aio.savedTo)); pmData.put("subject", Collections.singletonList(aio.savedSubject)); pmData.put("message", Collections.singletonList(aio.savedMessage)); pmData.put("submit", Collections.singletonList("Send Message")); post(NetDesc.PM_SEND_S2, "/pm/new", pmData); break; case PM_SEND_S2: if (doc.select("input[name=subject]").isEmpty()) { aio.pmCleanup(true, null); } else { String error = doc.select("form[action=/pm/new]").first().previousElementSibling().text(); aio.pmCleanup(false, error); } break; case TOPIC: if (BuildConfig.DEBUG) AllInOneV2.wtl("session hNR determined this is a topic"); processTopicsAndMessages(doc, resUrl, NetDesc.TOPIC); break; case MESSAGE_DETAIL: if (BuildConfig.DEBUG) AllInOneV2.wtl("session hNR determined this is a message"); processTopicsAndMessages(doc, resUrl, NetDesc.MESSAGE_DETAIL); break; case GAME_SEARCH: case BOARD_LIST: case AMP_LIST: case TRACKED_TOPICS: case BOARD: case BOARD_JUMPER: case UNSPECIFIED: case USER_DETAIL: case MODHIST: case PM_INBOX: case PM_INBOX_DETAIL: case PM_OUTBOX: case PM_OUTBOX_DETAIL: case VERIFY_ACCOUNT_S1: case VERIFY_ACCOUNT_S2: case NOTIFS_PAGE: case MENTIONS_PAGE: if (BuildConfig.DEBUG) AllInOneV2.wtl("session hNR determined this should be handled by AIO"); aio.processContent(desc, doc, resUrl); break; } } else { // connection failed for some reason, probably timed out if (BuildConfig.DEBUG) AllInOneV2.wtl("res was null in session hNR"); aio.timeoutCleanup(desc); } } catch (TimeoutException timeoutEx) { aio.timeoutCleanup(desc); } catch (UnknownHostException unknownHostEx) { aio.genError("Unknown Host Exception", "Couldn't find the address for the specified host. " + "This usually happens due to a DNS lookup error, which is outside of GameRaven's " + "ability to handle. If you continue to receive this error, try resetting your network. " + "If you are on wifi, you can do this by unplugging your router for 30 seconds, then plugging " + "it back in. If on a cellular connection, toggle airplane mode on and off, or restart " + "the phone.", "Ok"); } catch (ConnectionClosedException connClosedEx) { aio.genError("Connection Closed", "The connection was closed before the the response was completed.", "Ok"); } catch (Throwable ex) { ex.printStackTrace(); String url, body; if (result != null) { try { url = result.getRequest().getUri().toString(); } catch (Exception e1) { url = "uri is null"; } try { body = new String(result.getResult().bytes); } catch (Exception e1) { body = "result bytes are null"; } } else url = body = "res is null"; aio.tryCaught(url, desc.toString(), ex, body); } if (BuildConfig.DEBUG) AllInOneV2.wtl("session hNR finishing, desc: " + desc.name()); } private void addHistory() { if (lastPath != null) { switch (lastDesc) { case AMP_LIST: case TRACKED_TOPICS: case BOARD: case BOARD_JUMPER: case TOPIC: case GAME_SEARCH: case BOARD_LIST: case MESSAGE_DETAIL: case USER_DETAIL: case MODHIST: case PM_INBOX: case PM_INBOX_DETAIL: case PM_OUTBOX: case PM_OUTBOX_DETAIL: case MSG_DELETE: case NOTIFS_PAGE: case MENTIONS_PAGE: case UNSPECIFIED: if (BuildConfig.DEBUG) AllInOneV2.wtl("beginning history addition"); int[] vLoc = aio.getScrollerVertLoc(); hAdapter.insertHistory(lastPath, lastDesc.name(), lastResBodyAsBytes, vLoc[0], vLoc[1]); if (BuildConfig.DEBUG) AllInOneV2.wtl("finished history addition"); break; case USER_TAG: case MSG_MARK: case TOPIC_CLOSE: case LOGIN_S1: case LOGIN_S2: case EDIT_MSG: case MSG_POST_S1: case MSG_POST_S3: case TOPIC_POST_S1: case TOPIC_POST_S3: case VERIFY_ACCOUNT_S1: case VERIFY_ACCOUNT_S2: case PM_SEND_S1: case PM_SEND_S2: case NOTIFS_CLEAR: if (BuildConfig.DEBUG) AllInOneV2.wtl("not adding to history"); break; } } } private void processTopicsAndMessages(Document doc, String resUrl, NetDesc successDesc) { boolean processAsBoard = false; if (!doc.select("p:contains(no longer available for viewing)").isEmpty()) { if (successDesc == NetDesc.TOPIC) Crouton.showText(aio, "The topic you selected is no longer available for viewing.", Theming.croutonStyle()); else if (successDesc == NetDesc.MESSAGE_DETAIL) Crouton.showText(aio, "The message you selected is no longer available for viewing.", Theming.croutonStyle()); processAsBoard = true; } else if (!doc.select("p:contains(Your topic has been deleted)").isEmpty()) { Crouton.showText(aio, "Your topic has been deleted.", Theming.croutonStyle()); processAsBoard = true; } if (processAsBoard) { if (BuildConfig.DEBUG) AllInOneV2.wtl("topic or message is no longer available, treat response as a board"); aio.processContent(NetDesc.BOARD, doc, resUrl); } else { if (BuildConfig.DEBUG) AllInOneV2.wtl("handle the topic or message in AIO"); aio.processContent(successDesc, doc, resUrl); } } private boolean skipAIOCleanup = false; public void forceSkipAIOCleanup() { if (BuildConfig.DEBUG) AllInOneV2.wtl("forcing AIO cleanup skip"); skipAIOCleanup = true; } private boolean postErrorDetected = false; private void postExecuteCleanup(NetDesc desc) { switch (desc) { case AMP_LIST: case TRACKED_TOPICS: case BOARD: case BOARD_JUMPER: case TOPIC: case MESSAGE_DETAIL: case USER_DETAIL: case USER_TAG: case MODHIST: case PM_INBOX: case PM_INBOX_DETAIL: case PM_OUTBOX: case PM_OUTBOX_DETAIL: case MSG_MARK: case MSG_DELETE: case TOPIC_CLOSE: case GAME_SEARCH: case BOARD_LIST: case NOTIFS_PAGE: case MENTIONS_PAGE: case UNSPECIFIED: if (!skipAIOCleanup) aio.postExecuteCleanup(desc); break; case MSG_POST_S3: case TOPIC_POST_S3: if (!postErrorDetected) aio.postExecuteCleanup((desc == NetDesc.MSG_POST_S3 ? NetDesc.TOPIC : NetDesc.BOARD)); break; case LOGIN_S1: case LOGIN_S2: case EDIT_MSG: case MSG_POST_S1: case TOPIC_POST_S1: case VERIFY_ACCOUNT_S1: case VERIFY_ACCOUNT_S2: case PM_SEND_S1: case PM_SEND_S2: case NOTIFS_CLEAR: break; } skipAIOCleanup = false; postErrorDetected = false; } public boolean canGoBack() { return hAdapter.hasHistory(); } public void popHistory() { if (canGoBack()) hAdapter.pullHistory(); } public void goBack(boolean forceReload) { History h = hAdapter.pullHistory(); applySavedScroll = true; savedScrollVal = h.getVertPos(); if (forceReload || AllInOneV2.getSettingsPref().getBoolean("reloadOnBack", false)) { forceNoHistoryAddition(); if (BuildConfig.DEBUG) AllInOneV2.wtl("going back in history, refreshing: " + h.getDesc().name() + " " + h.getPath()); get(h.getDesc(), h.getPath()); } else { if (BuildConfig.DEBUG) AllInOneV2.wtl("going back in history: " + h.getDesc().name() + " " + h.getPath()); lastDesc = h.getDesc(); lastResBodyAsBytes = h.getResBodyAsBytes(); lastPath = h.getPath(); Document d = Jsoup.parse(new String(lastResBodyAsBytes), lastPath); d.outputSettings().syntax(Document.OutputSettings.Syntax.xml); aio.processContent(lastDesc, d, lastPath); } } public void openHistoryDB() { hAdapter.open(aio); } public void closeHistoryDB() { hAdapter.close(); } public void addHistoryBeforeStop() { addHistory(); } public void setLastPathAndDesc(String path, NetDesc desc) { lastPath = path; lastDesc = desc; } public void refresh() { forceNoHistoryAddition(); if (BuildConfig.DEBUG) AllInOneV2.wtl("refreshing: " + lastDesc.name() + " " + lastPath); applySavedScroll = true; savedScrollVal = aio.getScrollerVertLoc(); int i = lastPath.indexOf('#'); String trimmedPath; if (i != -1) trimmedPath = lastPath.substring(0, i); else trimmedPath = lastPath; if (lastDesc == NetDesc.AMP_LIST) trimmedPath = AllInOneV2.buildAMPLink(); get(lastDesc, trimmedPath); } private void showAutoFlagWarning(final String path, final HashMap<String, List<String>> data, final NetDesc desc, String msg) { AlertDialog.Builder b = new AlertDialog.Builder(aio); b.setTitle("Post Warning"); b.setMessage(msg); b.setPositiveButton("Post anyway", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { post(desc, path, data); } }); b.setNegativeButton("Cancel", new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { aio.postExecuteCleanup(desc); } }); Dialog d = b.create(); d.setCancelable(false); d.show(); } private void updateUserLevel(Document doc) { String sc = doc.getElementsByTag("head").first().getElementsByTag("script").html(); int start = sc.indexOf("UserLevel','") + 12; int end = sc.indexOf('\'', start + 1); if (end > start) userLevel = Integer.parseInt(sc.substring(start, end)); if (BuildConfig.DEBUG) AllInOneV2.wtl("user level: " + userLevel); } public static NetDesc determineNetDesc(String url) { url = Session.buildURL(url, NetDesc.UNSPECIFIED); if (url.startsWith(Session.ROOT)) { if (url.equals(Session.ROOT + "/pm")) url += "/"; if (url.contains("/pm/")) { if (url.contains("/pm/sent?id=")) { return NetDesc.PM_OUTBOX_DETAIL; } else if (url.contains("/pm/sent")) { return NetDesc.PM_OUTBOX; } else if (url.contains("?id=")) { return NetDesc.PM_INBOX_DETAIL; } else { return NetDesc.PM_INBOX; } } else if (url.contains("/user/")) { if (url.contains("/messages")) { return NetDesc.AMP_LIST; } else if (url.contains("/notifications")) { return NetDesc.NOTIFS_PAGE; } else if (url.contains("/mentions")) { return NetDesc.MENTIONS_PAGE; } else if (url.contains("/tracked")) { return NetDesc.TRACKED_TOPICS; } else if (url.contains("/moderated")) { return NetDesc.MODHIST; } else if (url.contains("/friends")) { return NetDesc.FRIENDS; } else if (url.contains("/following")) { return NetDesc.FOLLOWING; } else if (url.contains("/followers")) { return NetDesc.FOLLOWERS; } } else if (url.contains("/boards")) { if (url.contains("/users/")) { return NetDesc.USER_DETAIL; } else if (url.contains("boardlist.php")) { return NetDesc.BOARD_LIST; } else { String boardUrl = url.substring(url.indexOf("boards")); if (boardUrl.contains("/")) { String checkForTopicSep = boardUrl.substring(boardUrl.indexOf("/") + 1); if (checkForTopicSep.contains("/")) { String checkForMsgSep = checkForTopicSep.substring(checkForTopicSep.indexOf("/") + 1); if (checkForMsgSep.contains("/")) { // should be a message return NetDesc.MESSAGE_DETAIL; } else { // should be a topic return NetDesc.TOPIC; } } else { // should be a board return NetDesc.BOARD; } } else { // should be home return NetDesc.BOARD_JUMPER; } } } } return NetDesc.UNSPECIFIED; } }