diff --git a/Clover/app/libs/httpclientandroidlib-1.2.1.jar b/Clover/app/libs/httpclientandroidlib-1.2.1.jar deleted file mode 100644 index f409b0b2..00000000 Binary files a/Clover/app/libs/httpclientandroidlib-1.2.1.jar and /dev/null differ diff --git a/Clover/app/src/main/assets/captcha/captcha.html b/Clover/app/src/main/assets/captcha/captcha.html new file mode 100644 index 00000000..ad2a4ee6 --- /dev/null +++ b/Clover/app/src/main/assets/captcha/captcha.html @@ -0,0 +1,41 @@ + + + + + + + + +
Loading captcha...
+
+ + + + diff --git a/Clover/app/src/main/java/com/android/volley/toolbox/HurlStack.java b/Clover/app/src/main/java/com/android/volley/toolbox/HurlStack.java index 31d57f0d..7bd5bf7a 100644 --- a/Clover/app/src/main/java/com/android/volley/toolbox/HurlStack.java +++ b/Clover/app/src/main/java/com/android/volley/toolbox/HurlStack.java @@ -49,6 +49,7 @@ import javax.net.ssl.SSLSocketFactory; public class HurlStack implements HttpStack { private static final String HEADER_CONTENT_TYPE = "Content-Type"; + private final String mUserAgent; /** * An interface for transforming URLs before use. @@ -64,22 +65,23 @@ public class HurlStack implements HttpStack { private final UrlRewriter mUrlRewriter; private final SSLSocketFactory mSslSocketFactory; - public HurlStack() { - this(null); + public HurlStack(String userAgent) { + this(userAgent, null); } /** * @param urlRewriter Rewriter to use for request URLs */ - public HurlStack(UrlRewriter urlRewriter) { - this(urlRewriter, null); + public HurlStack(String userAgent, UrlRewriter urlRewriter) { + this(userAgent, urlRewriter, null); } /** * @param urlRewriter Rewriter to use for request URLs * @param sslSocketFactory SSL factory to use for HTTPS connections */ - public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) { + public HurlStack(String userAgent, UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) { + mUserAgent = userAgent; mUrlRewriter = urlRewriter; mSslSocketFactory = sslSocketFactory; } @@ -91,6 +93,7 @@ public class HurlStack implements HttpStack { HashMap map = new HashMap(); map.putAll(request.getHeaders()); map.putAll(additionalHeaders); + map.put("User-Agent", mUserAgent); if (mUrlRewriter != null) { String rewritten = mUrlRewriter.rewriteUrl(url); if (rewritten == null) { diff --git a/Clover/app/src/main/java/com/android/volley/toolbox/Volley.java b/Clover/app/src/main/java/com/android/volley/toolbox/Volley.java index 37c18a25..b840c6d1 100644 --- a/Clover/app/src/main/java/com/android/volley/toolbox/Volley.java +++ b/Clover/app/src/main/java/com/android/volley/toolbox/Volley.java @@ -17,8 +17,6 @@ package com.android.volley.toolbox; import android.content.Context; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; import android.net.http.AndroidHttpClient; import android.os.Build; @@ -33,38 +31,14 @@ public class Volley { /** Default on-disk cache directory. */ public static final String DEFAULT_CACHE_DIR = "volley"; - - /** - * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. - * - * @param context A {@link Context} to use for creating the cache dir. - * @param stack An {@link HttpStack} to use for the network, or null for default. - * @return A started {@link RequestQueue} instance. - */ - public static RequestQueue newRequestQueue(Context context, HttpStack stack) { - return newRequestQueue(context, stack, new File(context.getCacheDir(), DEFAULT_CACHE_DIR)); - } - - public static RequestQueue newRequestQueue(Context context, HttpStack stack, File cacheDir) { - return newRequestQueue(context, stack, cacheDir, -1); - } - - public static RequestQueue newRequestQueue(Context context, HttpStack stack, File cacheDir, int diskCacheSize) { - String userAgent = "volley/0"; - try { - String packageName = context.getPackageName(); - PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0); - userAgent = packageName + "/" + info.versionCode; - } catch (NameNotFoundException e) { - } - + public static RequestQueue newRequestQueue(Context context, String userAgent, HttpStack stack, File cacheDir, int diskCacheSize) { if (stack == null) { if (Build.VERSION.SDK_INT >= 9) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { // Use a socket factory that removes sslv3 - stack = new HurlStack(null, new NoSSLv3Compat.NoSSLv3Factory()); + stack = new HurlStack(userAgent, null, new NoSSLv3Compat.NoSSLv3Factory()); } else { - stack = new HurlStack(); + stack = new HurlStack(userAgent); } } else { // Prior to Gingerbread, HttpUrlConnection was unreliable. @@ -81,14 +55,4 @@ public class Volley { return queue; } - - /** - * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it. - * - * @param context A {@link Context} to use for creating the cache dir. - * @return A started {@link RequestQueue} instance. - */ - public static RequestQueue newRequestQueue(Context context) { - return newRequestQueue(context, null); - } } diff --git a/Clover/app/src/main/java/org/floens/chan/ChanApplication.java b/Clover/app/src/main/java/org/floens/chan/ChanApplication.java index 295e1d2e..6f6a6879 100644 --- a/Clover/app/src/main/java/org/floens/chan/ChanApplication.java +++ b/Clover/app/src/main/java/org/floens/chan/ChanApplication.java @@ -19,6 +19,7 @@ package org.floens.chan; import android.app.Application; import android.content.SharedPreferences; +import android.os.StrictMode; import android.preference.PreferenceManager; import android.view.ViewConfiguration; @@ -119,8 +120,8 @@ public class ChanApplication extends Application { } if (ChanBuild.DEVELOPER_MODE) { -// StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); -// StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build()); + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build()); } ChanUrls.loadScheme(ChanPreferences.getNetworkHttps()); @@ -129,7 +130,9 @@ public class ChanApplication extends Application { File cacheDir = getExternalCacheDir() != null ? getExternalCacheDir() : getCacheDir(); - volleyRequestQueue = Volley.newRequestQueue(this, null, new File(cacheDir, Volley.DEFAULT_CACHE_DIR), VOLLEY_CACHE_SIZE); + replyManager = new ReplyManager(this); + + volleyRequestQueue = Volley.newRequestQueue(this, replyManager.getUserAgent(), null, new File(cacheDir, Volley.DEFAULT_CACHE_DIR), VOLLEY_CACHE_SIZE); imageLoader = new ImageLoader(volleyRequestQueue, new BitmapLruImageCache(VOLLEY_LRU_CACHE_SIZE)); fileCache = new FileCache(new File(cacheDir, FILE_CACHE_NAME), FILE_CACHE_DISK_SIZE); @@ -137,7 +140,7 @@ public class ChanApplication extends Application { databaseManager = new DatabaseManager(this); boardManager = new BoardManager(); watchManager = new WatchManager(this); - replyManager = new ReplyManager(this); + } public void activityEnteredForeground() { diff --git a/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java b/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java index 2b640b7c..16e545d5 100644 --- a/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java +++ b/Clover/app/src/main/java/org/floens/chan/chan/ChanUrls.java @@ -38,12 +38,8 @@ public class ChanUrls { return scheme + "://a.4cdn.org/" + board + "/thread/" + no + ".json"; } - public static String getCaptchaDomain() { - return scheme + "://www.google.com/"; - } - - public static String getCaptchaFallback() { - return scheme + "://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"; + public static String getCaptchaSiteKey() { + return "6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc"; } public static String getImageUrl(String board, String code, String extension) { diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java index a8ea252a..aa2c79ed 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/ReplyManager.java @@ -23,6 +23,7 @@ import android.content.pm.PackageManager; import android.text.TextUtils; import com.squareup.okhttp.Callback; +import com.squareup.okhttp.FormEncodingBuilder; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.MultipartBuilder; import com.squareup.okhttp.OkHttpClient; @@ -33,58 +34,61 @@ import com.squareup.okhttp.Response; import org.floens.chan.ChanApplication; import org.floens.chan.R; import org.floens.chan.chan.ChanUrls; -import org.floens.chan.core.model.Pass; import org.floens.chan.core.model.Reply; import org.floens.chan.core.model.SavedReply; import org.floens.chan.ui.activity.ImagePickActivity; import org.floens.chan.utils.Logger; import org.floens.chan.utils.Utils; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; import java.io.File; import java.io.IOException; +import java.net.HttpCookie; +import java.util.List; import java.util.Locale; import java.util.Random; +import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; -import ch.boye.httpclientandroidlib.Consts; -import ch.boye.httpclientandroidlib.Header; -import ch.boye.httpclientandroidlib.HeaderElement; -import ch.boye.httpclientandroidlib.HttpResponse; -import ch.boye.httpclientandroidlib.client.HttpClient; -import ch.boye.httpclientandroidlib.client.config.RequestConfig; -import ch.boye.httpclientandroidlib.client.methods.CloseableHttpResponse; -import ch.boye.httpclientandroidlib.client.methods.HttpPost; -import ch.boye.httpclientandroidlib.entity.ContentType; -import ch.boye.httpclientandroidlib.entity.mime.MultipartEntityBuilder; -import ch.boye.httpclientandroidlib.impl.client.CloseableHttpClient; -import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder; -import ch.boye.httpclientandroidlib.util.EntityUtils; - /** * To send an reply to 4chan. */ public class ReplyManager { private static final String TAG = "ReplyManager"; - private static final Pattern responsePattern = Pattern.compile(""); - private static final int POST_TIMEOUT = 10000; - - private static final ContentType TEXT_UTF_8 = ContentType.create( - "text/plain", Consts.UTF_8); + private static final Pattern POST_THREAD_NO_PATTERN = Pattern.compile(""); + private static final int TIMEOUT = 10000; private final Context context; private Reply draft; private FileListener fileListener; private final Random random = new Random(); + private String userAgent; + OkHttpClient client; public ReplyManager(Context context) { this.context = context; draft = new Reply(); + + client = new OkHttpClient(); + client.setConnectTimeout(TIMEOUT, TimeUnit.MILLISECONDS); + client.setReadTimeout(TIMEOUT, TimeUnit.MILLISECONDS); + client.setWriteTimeout(TIMEOUT, TimeUnit.MILLISECONDS); + + // User agent is / + String version = ""; + try { + version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + + version = version.toLowerCase(Locale.ENGLISH).replace(" ", "_"); + userAgent = context.getString(R.string.app_name) + "/" + version; + } + + public String getUserAgent() { + return userAgent; } /** @@ -180,75 +184,80 @@ public class ReplyManager { public abstract void onFileLoading(); } - public void sendPass(Pass pass, final PassListener listener) { - Logger.i(TAG, "Sending pass login request"); - - HttpPost httpPost = new HttpPost(ChanUrls.getPassUrl()); + public void postPass(String token, String pin, final PassListener passListener) { + FormEncodingBuilder formBuilder = new FormEncodingBuilder(); - MultipartEntityBuilder entity = MultipartEntityBuilder.create(); + formBuilder.add("act", "do_login"); - entity.addTextBody("act", "do_login"); + formBuilder.add("id", token); + formBuilder.add("pin", pin); - entity.addTextBody("id", pass.token); - entity.addTextBody("pin", pass.pin); + Request.Builder request = new Request.Builder() + .url(ChanUrls.getPassUrl()) + .post(formBuilder.build()); - // entity.addPart("pwd", new StringBody(reply.password)); - - httpPost.setEntity(entity.build()); - - sendHttpPost(httpPost, new HttpPostSendListener() { + makeOkHttpCall(request, new Callback() { @Override - public void onResponse(String responseString, HttpClient client, HttpResponse response) { - PassResponse e = new PassResponse(); + public void onFailure(Request request, IOException e) { + final PassResponse res = new PassResponse(); + res.isError = true; + res.message = context.getString(R.string.pass_error); + runUI(new Runnable() { + public void run() { + passListener.onResponse(res); + } + }); + } - if (responseString == null || response == null) { - e.isError = true; - e.message = context.getString(R.string.pass_error); - } else { - e.responseData = responseString; - - if (responseString.contains("Your device is now authorized")) { - e.message = "Success! Your device is now authorized."; - - String passId = null; - - Header[] cookieHeaders = response.getHeaders("Set-Cookie"); - if (cookieHeaders != null) { - for (Header cookieHeader : cookieHeaders) { - HeaderElement[] elements = cookieHeader.getElements(); - if (elements != null) { - for (HeaderElement el : elements) { - if (el != null) { - if (el.getName().equals("pass_id")) { - passId = el.getValue(); - } - } - } + @Override + public void onResponse(Response response) throws IOException { + if (!response.isSuccessful()) { + onFailure(response.request(), null); + return; + } + String responseString = response.body().string(); + response.body().close(); + + final PassResponse res = new PassResponse(); + if (responseString.contains("Your device is now authorized")) { + List cookies = response.headers("Set-Cookie"); + String passId = null; + for (String cookie : cookies) { + try { + List parsedList = HttpCookie.parse(cookie); + for (HttpCookie parsed : parsedList) { + if (parsed.getName().equals("pass_id") && !parsed.getValue().equals("0")) { + passId = parsed.getValue(); } } + } catch (IllegalArgumentException ignored) { } - - if (passId != null) { - e.passId = passId; - } else { - e.isError = true; - e.message = "Could not get pass id"; - } + } + if (passId != null) { + res.passId = passId; + res.message = "Success! Your device is now authorized."; } else { - e.isError = true; - if (responseString.contains("Your Token must be exactly 10 characters")) { - e.message = "Incorrect token"; - } else if (responseString.contains("You have left one or more fields blank")) { - e.message = "You have left one or more fields blank"; - } else if (responseString.contains("Incorrect Token or PIN")) { - e.message = "Incorrect Token or PIN"; - } else { - e.unknownError = true; - } + res.isError = true; + res.message = "Could not get pass id"; + } + } else { + res.isError = true; + if (responseString.contains("Your Token must be exactly 10 characters")) { + res.message = "Incorrect token"; + } else if (responseString.contains("You have left one or more fields blank")) { + res.message = "You have left one or more fields blank"; + } else if (responseString.contains("Incorrect Token or PIN")) { + res.message = "Incorrect Token or PIN"; + } else { + res.unknownError = true; } } - listener.onResponse(e); + runUI(new Runnable() { + public void run() { + passListener.onResponse(res); + } + }); } }); } @@ -265,58 +274,68 @@ public class ReplyManager { public String passId; } - public void sendDelete(final SavedReply reply, boolean onlyImageDelete, final DeleteListener listener) { - Logger.i(TAG, "Sending delete request: " + reply.board + ", " + reply.no); - - HttpPost httpPost = new HttpPost(ChanUrls.getDeleteUrl(reply.board)); - - MultipartEntityBuilder entity = MultipartEntityBuilder.create(); - - - entity.addTextBody(Integer.toString(reply.no), "delete"); - + public void postDelete(final SavedReply reply, boolean onlyImageDelete, final DeleteListener listener) { + FormEncodingBuilder formBuilder = new FormEncodingBuilder(); + formBuilder.add(Integer.toString(reply.no), "delete"); if (onlyImageDelete) { - entity.addTextBody("onlyimgdel", "on"); + formBuilder.add("onlyimgdel", "on"); } + formBuilder.add("mode", "usrdel"); + formBuilder.add("pwd", reply.password); - // res not necessary + Request.Builder request = new Request.Builder() + .url(ChanUrls.getDeleteUrl(reply.board)) + .post(formBuilder.build()); - entity.addTextBody("mode", "usrdel"); - entity.addTextBody("pwd", reply.password); - - - httpPost.setEntity(entity.build()); - - sendHttpPost(httpPost, new HttpPostSendListener() { + makeOkHttpCall(request, new Callback() { @Override - public void onResponse(String responseString, HttpClient client, HttpResponse response) { - DeleteResponse e = new DeleteResponse(); - - if (responseString == null) { - e.isNetworkError = true; - } else { - e.responseData = responseString; - - if (responseString.contains("You must wait longer before deleting this post")) { - e.isUserError = true; - e.isTooSoonError = true; - } else if (responseString.contains("Password incorrect")) { - e.isUserError = true; - e.isInvalidPassword = true; - } else if (responseString.contains("You cannot delete a post this old")) { - e.isUserError = true; - e.isTooOldError = true; - } else if (responseString.contains("Updating index")) { - e.isSuccessful = true; + public void onFailure(Request request, IOException e) { + final DeleteResponse res = new DeleteResponse(); + res.isNetworkError = true; + runUI(new Runnable() { + @Override + public void run() { + listener.onResponse(res); } + }); + } + + @Override + public void onResponse(Response response) throws IOException { + if (!response.isSuccessful()) { + onFailure(response.request(), null); + return; + } + String responseString = response.body().string(); + response.body().close(); + + final DeleteResponse res = new DeleteResponse(); + res.responseData = responseString; + + if (responseString.contains("You must wait longer before deleting this post")) { + res.isUserError = true; + res.isTooSoonError = true; + } else if (responseString.contains("Password incorrect")) { + res.isUserError = true; + res.isInvalidPassword = true; + } else if (responseString.contains("You cannot delete a post this old")) { + res.isUserError = true; + res.isTooOldError = true; + } else if (responseString.contains("Updating index")) { + res.isSuccessful = true; } - listener.onResponse(e); + runUI(new Runnable() { + @Override + public void run() { + listener.onResponse(res); + } + }); } }); } - public static interface DeleteListener { + public interface DeleteListener { public void onResponse(DeleteResponse response); } @@ -330,244 +349,139 @@ public class ReplyManager { public String responseData = ""; } - public void getCaptchaChallenge(final CaptchaChallengeListener listener) { - HttpPost httpPost = new HttpPost(ChanUrls.getCaptchaFallback()); - httpPost.addHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.101 Safari/537.36"); - httpPost.addHeader("Referer", "https://boards.4chan.org/"); - httpPost.addHeader("Cookie", "NID=67"); - - HttpPostSendListener postListener = new HttpPostSendListener() { - @Override - public void onResponse(String responseString, HttpClient client, HttpResponse response) { - if (responseString != null) { - Document document = Jsoup.parseBodyFragment(responseString, ChanUrls.getCaptchaDomain()); - Elements images = document.select("div.fbc-challenge img"); - String imageUrl = images.first() == null ? "" : images.first().absUrl("src"); - - Elements inputs = document.select("div.fbc-challenge input"); - String challenge = ""; - for (Element input : inputs) { - if (input.attr("name").equals("c")) { - challenge = input.attr("value"); - break; - } - } - - if (!TextUtils.isEmpty(imageUrl) && !TextUtils.isEmpty(challenge)) { - listener.onChallenge(imageUrl, challenge); - return; - } - } - listener.onError(); - } - }; - - sendHttpPost(httpPost, postListener); + public void postReply(Reply reply, ReplyListener replyListener) { + if (reply.usePass) { + postReplyInternal(reply, replyListener, null); + } else { + postReplyInternal(reply, replyListener, reply.captchaResponse); + } } - public interface CaptchaChallengeListener { - public void onChallenge(String imageUrl, String challenge); + private void postReplyInternal(final Reply reply, final ReplyListener replyListener, String captchaHash) { + reply.password = Long.toHexString(random.nextLong()); - public void onError(); - } + MultipartBuilder formBuilder = new MultipartBuilder(); + formBuilder.type(MultipartBuilder.FORM); - private void getCaptchaHash(final CaptchaHashListener listener, String challenge, String response) { - HttpPost httpPost = new HttpPost(ChanUrls.getCaptchaFallback()); + formBuilder.addFormDataPart("mode", "regist"); + formBuilder.addFormDataPart("pwd", reply.password); - MultipartEntityBuilder entity = MultipartEntityBuilder.create(); + if (reply.resto >= 0) { + formBuilder.addFormDataPart("resto", String.valueOf(reply.resto)); + } - entity.addTextBody("c", challenge, TEXT_UTF_8); - entity.addTextBody("response", response, TEXT_UTF_8); + formBuilder.addFormDataPart("name", reply.name); + formBuilder.addFormDataPart("email", reply.email); - httpPost.setEntity(entity.build()); + if (!TextUtils.isEmpty(reply.subject)) { + formBuilder.addFormDataPart("sub", reply.subject); + } - sendHttpPost(httpPost, new HttpPostSendListener() { - @Override - public void onResponse(String responseString, HttpClient client, HttpResponse response) { - if (responseString != null) { - Document document = Jsoup.parseBodyFragment(responseString); - Elements verificationToken = document.select("div.fbc-verification-token textarea"); - String hash = verificationToken.text(); - if (hash.length() > 0) { - listener.onHash(hash); - return; - } - } - listener.onHash(null); - } - }); - } + formBuilder.addFormDataPart("com", reply.comment); - private interface CaptchaHashListener { - public void onHash(String hash); - } - - public String getUserAgent() { - String version = ""; - try { - version = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName; - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); + if (captchaHash != null) { + formBuilder.addFormDataPart("g-recaptcha-response", captchaHash); } - version = version.toLowerCase(Locale.ENGLISH).replace(" ", "_"); - return "Clover/" + version; - } - public void postReply(Reply reply) { - OkHttpClient client = new OkHttpClient(); - - MultipartBuilder formBuilder = new MultipartBuilder(); - formBuilder.type(MultipartBuilder.FORM); - formBuilder.addFormDataPart("foo", "bar"); if (reply.file != null) { - formBuilder.addFormDataPart("file", "filename", RequestBody.create( + formBuilder.addFormDataPart("upfile", reply.fileName, RequestBody.create( MediaType.parse("application/octet-stream"), reply.file )); } - RequestBody form = formBuilder.build(); - Request request = new Request.Builder() - .header("User-Agent", getUserAgent()) - .url("") - .post(form) - .build(); + if (reply.spoilerImage) { + formBuilder.addFormDataPart("spoiler", "on"); + } + + Request.Builder request = new Request.Builder() + .url(ChanUrls.getReplyUrl(reply.board)) + .post(formBuilder.build()); + + if (reply.usePass) { + request.addHeader("Cookie", "pass_id=" + reply.passId); + } - client.newCall(request).enqueue(new Callback() { + makeOkHttpCall(request, new Callback() { @Override public void onFailure(Request request, IOException e) { - e.printStackTrace(); + final ReplyResponse res = new ReplyResponse(); + res.isNetworkError = true; + + runUI(new Runnable() { + public void run() { + replyListener.onResponse(res); + } + }); } @Override public void onResponse(Response response) throws IOException { + final ReplyResponse res = new ReplyResponse(); if (response.isSuccessful()) { - Logger.test("Output = " + response.body().string()); + onReplyPosted(response.body().string(), reply, res); + response.body().close(); + } else { + res.isNetworkError = true; } + + runUI(new Runnable() { + public void run() { + replyListener.onResponse(res); + } + }); } }); } - /** - * Send an reply off to the server. - * - * @param reply The reply object with all data needed, like captcha and the - * file. - * @param listener The listener, after server response. - */ - public void sendReply(final Reply reply, final ReplyListener listener) { - Logger.i(TAG, "Sending reply request: " + reply.board + ", " + reply.resto); - - CaptchaHashListener captchaHashListener = new CaptchaHashListener() { - @Override - public void onHash(String captchaHash) { - if (captchaHash == null && !reply.usePass) { - // Could not find a hash in the response html - ReplyResponse e = new ReplyResponse(); - e.isUserError = true; - e.isCaptchaError = true; - listener.onResponse(e); - return; - } - - HttpPost httpPost = new HttpPost(ChanUrls.getReplyUrl(reply.board)); - - MultipartEntityBuilder entity = MultipartEntityBuilder.create(); - - reply.password = Long.toHexString(random.nextLong()); - - entity.addTextBody("name", reply.name, TEXT_UTF_8); - entity.addTextBody("email", reply.email, TEXT_UTF_8); - - entity.addTextBody("sub", reply.subject, TEXT_UTF_8); - entity.addTextBody("com", reply.comment, TEXT_UTF_8); - - if (reply.resto >= 0) { - entity.addTextBody("resto", Integer.toString(reply.resto)); - } - - if (reply.spoilerImage) { - entity.addTextBody("spoiler", "on"); - } - - if (!reply.usePass) { - entity.addTextBody("g-recaptcha-response", captchaHash, TEXT_UTF_8); - } - - entity.addTextBody("mode", "regist"); - entity.addTextBody("pwd", reply.password); - - if (reply.usePass) { - httpPost.addHeader("Cookie", "pass_id=" + reply.passId); - } - - if (reply.file != null) { - entity.addBinaryBody("upfile", reply.file, ContentType.APPLICATION_OCTET_STREAM, reply.fileName); + private ReplyResponse onReplyPosted(String responseString, Reply reply, ReplyResponse res) { + res.responseData = responseString; + + if (res.responseData.contains("No file selected")) { + res.isUserError = true; + res.isFileError = true; + } else if (res.responseData.contains("You forgot to solve the CAPTCHA") || res.responseData.contains("You seem to have mistyped the CAPTCHA")) { + res.isUserError = true; + res.isCaptchaError = true; + } else if (res.responseData.toLowerCase(Locale.ENGLISH).contains("post successful")) { + res.isSuccessful = true; + + Matcher matcher = POST_THREAD_NO_PATTERN.matcher(res.responseData); + int threadNo = -1; + int no = -1; + if (matcher.find()) { + try { + threadNo = Integer.parseInt(matcher.group(1)); + no = Integer.parseInt(matcher.group(2)); + } catch (NumberFormatException err) { + err.printStackTrace(); } + } - httpPost.setEntity(entity.build()); - - sendHttpPost(httpPost, new HttpPostSendListener() { - @Override - public void onResponse(String responseString, HttpClient client, HttpResponse response) { - ReplyResponse e = new ReplyResponse(); - - if (responseString == null) { - e.isNetworkError = true; - } else { - e.responseData = responseString; - - if (responseString.contains("No file selected")) { - e.isUserError = true; - e.isFileError = true; - } else if (responseString.contains("You forgot to solve the CAPTCHA") - || responseString.contains("You seem to have mistyped the CAPTCHA")) { - e.isUserError = true; - e.isCaptchaError = true; - } else if (responseString.toLowerCase(Locale.ENGLISH).contains("post successful")) { - e.isSuccessful = true; - } - } - - if (e.isSuccessful) { - Matcher matcher = responsePattern.matcher(e.responseData); - - int threadNo = -1; - int no = -1; - if (matcher.find()) { - try { - threadNo = Integer.parseInt(matcher.group(1)); - no = Integer.parseInt(matcher.group(2)); - } catch (NumberFormatException err) { - err.printStackTrace(); - } - } - - if (threadNo >= 0 && no >= 0) { - SavedReply savedReply = new SavedReply(); - savedReply.board = reply.board; - savedReply.no = no; - savedReply.password = reply.password; - - ChanApplication.getDatabaseManager().saveReply(savedReply); + if (threadNo >= 0 && no >= 0) { + SavedReply savedReply = new SavedReply(); + savedReply.board = reply.board; + savedReply.no = no; + savedReply.password = reply.password; - e.threadNo = threadNo; - e.no = no; - } else { - Logger.w(TAG, "No thread & no in the response"); - } - } + ChanApplication.getDatabaseManager().saveReply(savedReply); - listener.onResponse(e); - } - }); + res.threadNo = threadNo; + res.no = no; + } else { + Logger.w(TAG, "No thread & no in the response"); } - }; - - if (reply.usePass) { - captchaHashListener.onHash(null); - } else { - getCaptchaHash(captchaHashListener, reply.captchaChallenge, reply.captchaResponse); } + return res; + } + + private void makeOkHttpCall(Request.Builder requestBuilder, Callback callback) { + requestBuilder.header("User-Agent", getUserAgent()); + Request request = requestBuilder.build(); + client.newCall(request).enqueue(callback); + } + + private void runUI(Runnable runnable) { + Utils.runOnUiThread(runnable); } public static interface ReplyListener { @@ -616,55 +530,4 @@ public class ReplyManager { */ public int threadNo = -1; } - - /** - * Async task to send an reply to the server. Uses HttpClient. Since Android - * 4.4 there is an updated version of HttpClient, 4.2, given with Android. - * However, that version causes problems with file uploading. Version 4.3 of - * HttpClient has been given with a library, that has another namespace: - * ch.boye.httpclientandroidlib This lib also has some fixes/improvements of - * HttpClient for Android. - */ - private void sendHttpPost(final HttpPost post, final HttpPostSendListener listener) { - new Thread(new Runnable() { - @Override - public void run() { - RequestConfig.Builder requestBuilder = RequestConfig.custom(); - requestBuilder = requestBuilder.setConnectTimeout(POST_TIMEOUT); - requestBuilder = requestBuilder.setConnectionRequestTimeout(POST_TIMEOUT); - - HttpClientBuilder httpBuilder = HttpClientBuilder.create(); - httpBuilder.setDefaultRequestConfig(requestBuilder.build()); - final CloseableHttpClient client = httpBuilder.build(); - try { - final CloseableHttpResponse response = client.execute(post); - final String responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); - Utils.runOnUiThread(new Runnable() { - @Override - public void run() { - listener.onResponse(responseString, client, response); - } - }); - } catch (IOException e) { - e.printStackTrace(); - Utils.runOnUiThread(new Runnable() { - @Override - public void run() { - listener.onResponse(null, client, null); - } - }); - } finally { - try { - client.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - }).start(); - } - - private static interface HttpPostSendListener { - public void onResponse(String responseString, HttpClient client, HttpResponse response); - } } diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java index e6cd20b0..eb165c5a 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java +++ b/Clover/app/src/main/java/org/floens/chan/core/manager/ThreadManager.java @@ -547,7 +547,7 @@ public class ThreadManager implements Loader.LoaderListener { final ProgressDialog dialog = ProgressDialog.show(activity, null, activity.getString(R.string.delete_wait)); - ChanApplication.getReplyManager().sendDelete(reply, onlyImageDelete, new DeleteListener() { + ChanApplication.getReplyManager().postDelete(reply, onlyImageDelete, new DeleteListener() { @Override public void onResponse(DeleteResponse response) { dialog.dismiss(); diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Pass.java b/Clover/app/src/main/java/org/floens/chan/core/model/Pass.java deleted file mode 100644 index 5f5317dd..00000000 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Pass.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Clover - 4chan browser https://github.com/Floens/Clover/ - * Copyright (C) 2014 Floens - * - * 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 3 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, see . - */ -package org.floens.chan.core.model; - -public class Pass { - public String token = ""; - public String pin = ""; - - public Pass(String token, String pin) { - this.token = token; - this.pin = pin; - } -} diff --git a/Clover/app/src/main/java/org/floens/chan/core/model/Reply.java b/Clover/app/src/main/java/org/floens/chan/core/model/Reply.java index e2781b56..4f8c3aee 100644 --- a/Clover/app/src/main/java/org/floens/chan/core/model/Reply.java +++ b/Clover/app/src/main/java/org/floens/chan/core/model/Reply.java @@ -31,7 +31,6 @@ public class Reply { public int resto = 0; public File file; public String fileName = ""; - public String captchaChallenge = ""; public String captchaResponse = ""; public String password = ""; public boolean usePass = false; diff --git a/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java b/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java index 37bd50fa..c64b6ad6 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/activity/PassSettingsActivity.java @@ -42,7 +42,6 @@ import org.floens.chan.R; import org.floens.chan.core.ChanPreferences; import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.manager.ReplyManager.PassResponse; -import org.floens.chan.core.model.Pass; import org.floens.chan.utils.ThemeHelper; import org.floens.chan.utils.Utils; @@ -145,8 +144,7 @@ public class PassSettingsActivity extends Activity implements OnCheckedChangeLis @Override public boolean onPreferenceClick(Preference preference) { if (PassSettingsActivity.instance != null) { - Pass pass = new Pass(ChanPreferences.getPassToken(), ChanPreferences.getPassPin()); - onLoginClick(pass); + onLoginClick(ChanPreferences.getPassToken(), ChanPreferences.getPassPin()); } return true; } @@ -159,12 +157,12 @@ public class PassSettingsActivity extends Activity implements OnCheckedChangeLis findPreference("preference_pass_login").setTitle(TextUtils.isEmpty(ChanPreferences.getPassId()) ? R.string.pass_login : R.string.pass_logout); } - private void onLoginClick(Pass pass) { + private void onLoginClick(String token, String pin) { if (TextUtils.isEmpty(ChanPreferences.getPassId())) { // Login final ProgressDialog dialog = ProgressDialog.show(getActivity(), null, "Logging in"); - ChanApplication.getReplyManager().sendPass(pass, new ReplyManager.PassListener() { + ChanApplication.getReplyManager().postPass(token, pin, new ReplyManager.PassListener() { @Override public void onResponse(PassResponse response) { dialog.dismiss(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java index 7caee9a8..ab778e15 100644 --- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java +++ b/Clover/app/src/main/java/org/floens/chan/ui/fragment/ReplyFragment.java @@ -44,10 +44,9 @@ import android.widget.TextView; import android.widget.Toast; import android.widget.ViewFlipper; -import com.android.volley.toolbox.NetworkImageView; - import org.floens.chan.ChanApplication; import org.floens.chan.R; +import org.floens.chan.chan.ChanUrls; import org.floens.chan.core.ChanPreferences; import org.floens.chan.core.manager.ReplyManager; import org.floens.chan.core.manager.ReplyManager.ReplyResponse; @@ -55,6 +54,7 @@ import org.floens.chan.core.model.Board; import org.floens.chan.core.model.Loadable; import org.floens.chan.core.model.Reply; import org.floens.chan.ui.ViewFlipperAnimations; +import org.floens.chan.ui.layout.CaptchaLayout; import org.floens.chan.ui.view.LoadView; import org.floens.chan.utils.ImageDecoder; import org.floens.chan.utils.Logger; @@ -63,7 +63,7 @@ import org.floens.chan.utils.Utils; import java.io.File; -public class ReplyFragment extends DialogFragment { +public class ReplyFragment extends DialogFragment implements CaptchaLayout.CaptchaCallback { private static final String TAG = "ReplyFragment"; private int page = 0; @@ -74,9 +74,7 @@ public class ReplyFragment extends DialogFragment { private final Reply draft = new Reply(); private boolean shouldSaveDraft = true; - private boolean gotInitialCaptcha = false; - private boolean gettingCaptcha = false; - private String captchaChallenge = ""; + private String captchaResponse; private int defaultTextColor; private int maxCommentCount; @@ -94,8 +92,7 @@ public class ReplyFragment extends DialogFragment { private EditText fileNameView; private CheckBox spoilerImageView; private LoadView imageViewContainer; - private LoadView captchaContainer; - private TextView captchaInput; + private CaptchaLayout captchaLayout; private LoadView responseContainer; private Button insertSpoiler; private Button insertCode; @@ -204,6 +201,9 @@ public class ReplyFragment extends DialogFragment { } }); showCommentCount(); + + String baseUrl = loadable.isThreadMode() ? ChanUrls.getThreadUrlDesktop(loadable.board, loadable.no) : ChanUrls.getBoardUrlDesktop(loadable.board); + captchaLayout.initCaptcha(baseUrl, ChanUrls.getCaptchaSiteKey(), ThemeHelper.getInstance().getTheme().isLightTheme, this); } else { Logger.e(TAG, "Loadable in ReplyFragment was null"); closeReply(); @@ -258,14 +258,7 @@ public class ReplyFragment extends DialogFragment { imageViewContainer = (LoadView) container.findViewById(R.id.reply_image); responseContainer = (LoadView) container.findViewById(R.id.reply_response); - captchaContainer = (LoadView) container.findViewById(R.id.reply_captcha_container); - captchaContainer.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View view) { - getCaptcha(); - } - }); - captchaInput = (TextView) container.findViewById(R.id.reply_captcha); + captchaLayout = (CaptchaLayout) container.findViewById(R.id.captcha_layout); cancelButton = (Button) container.findViewById(R.id.reply_cancel); cancelButton.setOnClickListener(new OnClickListener() { @@ -347,6 +340,19 @@ public class ReplyFragment extends DialogFragment { } } + @Override + public void captchaLoaded(CaptchaLayout captchaLayout) { + } + + @Override + public void captchaEntered(CaptchaLayout captchaLayout, String response) { + captchaResponse = response; + if (page == 1) { + flipPage(2); + submit(); + } + } + private void insertAtCursor(String before, String after) { int pos = commentView.getSelectionStart(); String text = commentView.getText().toString(); @@ -412,9 +418,11 @@ public class ReplyFragment extends DialogFragment { cancelButton.setText(R.string.close); } - if (page == 1 && !gotInitialCaptcha) { - gotInitialCaptcha = true; - getCaptcha(); + if (page == 1) { + captchaLayout.load(); + submitButton.setEnabled(captchaResponse != null); + } else if (page == 0) { + submitButton.setEnabled(true); } } @@ -511,42 +519,6 @@ public class ReplyFragment extends DialogFragment { loadView.setView(text); } - private void getCaptcha() { - if (gettingCaptcha) - return; - gettingCaptcha = true; - - captchaContainer.setView(null); - captchaInput.setText(""); - - ChanApplication.getReplyManager().getCaptchaChallenge(new ReplyManager.CaptchaChallengeListener() { - @Override - public void onChallenge(String imageUrl, String challenge) { - gettingCaptcha = false; - - if (context != null) { - captchaChallenge = challenge; - - NetworkImageView captchaImage = new NetworkImageView(context); - captchaImage.setImageUrl(imageUrl, ChanApplication.getVolleyImageLoader()); - captchaContainer.setView(captchaImage); - } - } - - @Override - public void onError() { - gettingCaptcha = false; - - if (context != null) { - TextView text = new TextView(context); - text.setGravity(Gravity.CENTER); - text.setText(R.string.reply_captcha_load_error); - captchaContainer.setView(text); - } - } - }); - } - /** * Submit button clicked at page 1 */ @@ -561,8 +533,7 @@ public class ReplyFragment extends DialogFragment { draft.email = emailView.getText().toString(); draft.subject = subjectView.getText().toString(); draft.comment = commentView.getText().toString(); - draft.captchaChallenge = captchaChallenge; - draft.captchaResponse = captchaInput.getText().toString(); + draft.captchaResponse = captchaResponse; draft.fileName = "image"; String n = fileNameView.getText().toString(); @@ -581,13 +552,12 @@ public class ReplyFragment extends DialogFragment { Board b = ChanApplication.getBoardManager().getBoardByValue(loadable.board); draft.spoilerImage = b != null && b.spoilers && spoilerImageView.isChecked(); - /*ChanApplication.getReplyManager().sendReply(draft, new ReplyManager.ReplyListener() { + ChanApplication.getReplyManager().postReply(draft, new ReplyManager.ReplyListener() { @Override public void onResponse(ReplyResponse response) { handleSubmitResponse(response); } - });*/ - ChanApplication.getReplyManager().postReply(draft); + }); } /** @@ -606,13 +576,13 @@ public class ReplyFragment extends DialogFragment { submitButton.setEnabled(true); cancelButton.setEnabled(true); setClosable(true); + captchaResponse = null; + captchaLayout.reset(); if (ChanPreferences.getPassEnabled()) { flipPage(0); } else { flipPage(1); } - getCaptcha(); - captchaInput.setText(""); } else if (response.isSuccessful) { shouldSaveDraft = false; Toast.makeText(context, R.string.reply_success, Toast.LENGTH_SHORT).show(); diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java new file mode 100644 index 00000000..95a8e03f --- /dev/null +++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/CaptchaLayout.java @@ -0,0 +1,130 @@ +/* + * Clover - 4chan browser https://github.com/Floens/Clover/ + * Copyright (C) 2014 Floens + * + * 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 3 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, see . + */ +package org.floens.chan.ui.layout; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.webkit.JavascriptInterface; +import android.webkit.WebSettings; +import android.webkit.WebView; + +import org.floens.chan.ChanApplication; +import org.floens.chan.utils.IOUtils; +import org.floens.chan.utils.Utils; + +public class CaptchaLayout extends WebView { + private CaptchaCallback callback; + private boolean loaded = false; + private String baseUrl; + private String siteKey; + private boolean lightTheme; + + public CaptchaLayout(Context context) { + super(context); + } + + public CaptchaLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public CaptchaLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @SuppressLint("SetJavaScriptEnabled") + public void initCaptcha(String baseUrl, String siteKey, boolean lightTheme, CaptchaCallback callback) { + this.callback = callback; + this.baseUrl = baseUrl; + this.siteKey = siteKey; + this.lightTheme = lightTheme; + + WebSettings settings = getSettings(); + settings.setJavaScriptEnabled(true); + settings.setUserAgentString(ChanApplication.getReplyManager().getUserAgent()); + + addJavascriptInterface(new CaptchaInterface(this), "CaptchaCallback"); + } + + public void load() { + if (!loaded) { + loaded = true; + + String html = IOUtils.assetAsString(getContext(), "captcha/captcha.html"); + html = html.replace("__site_key__", siteKey); + html = html.replace("__theme__", lightTheme ? "light" : "dark"); + + loadDataWithBaseURL(baseUrl, html, "text/html", "UTF-8", null); + } + } + + public void reset() { + if (loaded) { + loadUrl("javascript:grecaptcha.reset()"); + } else { + load(); + } + } + + private void onCaptchaLoaded() { + callback.captchaLoaded(this); + } + + private void onCaptchaEntered(String response) { + if (TextUtils.isEmpty(response)) { + reset(); + } else { + callback.captchaEntered(this, response); + } + } + + public interface CaptchaCallback { + public void captchaLoaded(CaptchaLayout captchaLayout); + + public void captchaEntered(CaptchaLayout captchaLayout, String response); + } + + public static class CaptchaInterface { + private final CaptchaLayout layout; + + public CaptchaInterface(CaptchaLayout layout) { + this.layout = layout; + } + + @JavascriptInterface + public void onCaptchaLoaded() { + Utils.runOnUiThread(new Runnable() { + @Override + public void run() { + layout.onCaptchaLoaded(); + } + }); + } + + @JavascriptInterface + public void onCaptchaEntered(final String response) { + Utils.runOnUiThread(new Runnable() { + @Override + public void run() { + layout.onCaptchaEntered(response); + } + }); + } + } +} diff --git a/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java b/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java index 7e4c00be..8b4cb7fd 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/FileCache.java @@ -9,6 +9,8 @@ import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; import com.squareup.okhttp.internal.Util; +import org.floens.chan.ChanApplication; + import java.io.BufferedOutputStream; import java.io.Closeable; import java.io.File; @@ -25,8 +27,8 @@ public class FileCache { private static final String TAG = "FileCache"; private static final ExecutorService executor = Executors.newFixedThreadPool(2); - private OkHttpClient httpClient; + private static String userAgent; private final File directory; private final long maxSize; @@ -38,6 +40,7 @@ public class FileCache { this.maxSize = maxSize; httpClient = new OkHttpClient(); + userAgent = ChanApplication.getReplyManager().getUserAgent(); makeDir(); calculateSize(); @@ -271,7 +274,10 @@ public class FileCache { } private void execute() throws Exception { - Request request = new Request.Builder().url(url).build(); + Request request = new Request.Builder() + .url(url) + .header("User-Agent", FileCache.userAgent) + .build(); call = fileCache.httpClient.newCall(request); Response response = call.execute(); diff --git a/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java index 1e7ff38d..074d729c 100644 --- a/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java +++ b/Clover/app/src/main/java/org/floens/chan/utils/IOUtils.java @@ -17,6 +17,9 @@ */ package org.floens.chan.utils; +import android.content.Context; + +import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -26,20 +29,40 @@ import java.io.StringWriter; import java.io.Writer; public class IOUtils { + public static String assetAsString(Context context, String assetName) { + String res = null; + try { + res = IOUtils.readString(context.getResources().getAssets().open(assetName)); + } catch (IOException ignored) { + } + return res; + } + public static String readString(InputStream is) { - StringWriter sw = new StringWriter(); + Reader sr = new InputStreamReader(is); + Writer sw = new StringWriter(); try { - copy(new InputStreamReader(is), sw); - is.close(); - sw.close(); + copy(sr, sw); } catch (IOException e) { e.printStackTrace(); + } finally { + IOUtils.closeQuietly(sr); + IOUtils.closeQuietly(sw); } return sw.toString(); } + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException ignored) { + } + } + } + /** * Copies the inputstream to the outputstream and closes both streams. * diff --git a/Clover/app/src/main/res/layout/reply_captcha.xml b/Clover/app/src/main/res/layout/reply_captcha.xml deleted file mode 100644 index 776f9d81..00000000 --- a/Clover/app/src/main/res/layout/reply_captcha.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - diff --git a/Clover/app/src/main/res/layout/reply_view.xml b/Clover/app/src/main/res/layout/reply_view.xml index 52ba6f36..561e8795 100644 --- a/Clover/app/src/main/res/layout/reply_view.xml +++ b/Clover/app/src/main/res/layout/reply_view.xml @@ -44,12 +44,10 @@ along with this program. If not, see . - - - - + android:layout_height="match_parent" /> diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml index 3f2d2d6a..0dcab0b7 100644 --- a/Clover/app/src/main/res/values/strings.xml +++ b/Clover/app/src/main/res/values/strings.xml @@ -133,7 +133,7 @@ along with this program. If not, see . Submit Enter the text Error sending reply - Wrong captcha + Invalid captcha No file selected Post Successful Failed to load captcha