diff --git a/CHANGES.txt b/CHANGES.txt index 784b9266..fc98a0bc 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,19 @@ +New in 1.2.9 (2015-02-06) +- More captcha fixing, should be easier now. + + +New in 1.2.8 (2015-02-06) +- Fix captcha not working + + +New in 1.2.7 (2015-01-31) +- Fix some image loading issues + + +New in 1.2.6 (2015-01-28) +- Fix for captcha image not loading + + New in 1.2.5 (2014-12-11) - Fix 4chan pass diff --git a/Clover/app/build.gradle b/Clover/app/build.gradle index d42dc79a..9bafb187 100644 --- a/Clover/app/build.gradle +++ b/Clover/app/build.gradle @@ -2,14 +2,14 @@ apply plugin: 'com.android.application' android { compileSdkVersion 21 - buildToolsVersion "21.1.1" + buildToolsVersion "21.1.2" defaultConfig { minSdkVersion 14 targetSdkVersion 21 - versionName "v1.2.5" - versionCode 43 + versionName "v1.2.9" + versionCode 48 } compileOptions { @@ -78,8 +78,8 @@ dependencies { compile 'org.jsoup:jsoup:1.8.1' compile 'com.j256.ormlite:ormlite-core:4.48' compile 'com.j256.ormlite:ormlite-android:4.48' - compile 'com.koushikdutta.ion:ion:2.0.1' compile 'pl.droidsonroids.gif:android-gif-drawable:1.1.0' + compile 'com.squareup.okhttp:okhttp:2.2.0' compile files('libs/httpclientandroidlib-1.2.1.jar') } diff --git a/Clover/app/proguard.cfg b/Clover/app/proguard.cfg index a43aa602..b1606d14 100644 --- a/Clover/app/proguard.cfg +++ b/Clover/app/proguard.cfg @@ -131,5 +131,7 @@ public *; } --keep public class pl.droidsonroids.gif.GifIOException{(int);} --keep class pl.droidsonroids.gif.GifInfoHandle{(long,int,int,int);} +-keep public class pl.droidsonroids.gif.GifIOException{*;} + +-dontwarn java.nio.** +-dontwarn org.codehaus.mojo.** diff --git a/Clover/app/src/main/assets/html/licenses.html b/Clover/app/src/main/assets/html/licenses.html index 0d1b33ea..7240eb1f 100644 --- a/Clover/app/src/main/assets/html/licenses.html +++ b/Clover/app/src/main/assets/html/licenses.html @@ -38,34 +38,17 @@ limitations under the License.
-

Jsoup

-
-        
-The jsoup code-base (include source and compiled packages) are distributed under the open source MIT license as described below.
-
-The MIT License
-Copyright © 2009 - 2013 Jonathan Hedley (jonathan@hedley.net)
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-        
-    
-
- -

AndroidFloatLabel from Weddingparty

- https://github.com/weddingparty/AndroidFloatLabel +

subsampling-scale-image-view

+ https://github.com/davemorrissey/subsampling-scale-image-view
         
-Copyright (C) 2014 AndroidFloatLabel team
+Copyright 2014 David Morrissey
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
-  http://www.apache.org/licenses/LICENSE-2.0
+   http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -76,17 +59,14 @@ limitations under the License.
     

-

This software includes several Android classes from DashClock

- https://code.google.com/p/dashclock/ +

Square okhttp

         
-Copyright 2013 Google Inc.
-
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
- http://www.apache.org/licenses/LICENSE-2.0
+   http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -97,41 +77,34 @@ limitations under the License.
     

-

DragSortListView

- https://github.com/bauerca/drag-sort-listview +

Jsoup

         
-A subclass of the Android ListView component that enables drag
-and drop re-ordering of list items.
+The jsoup code-base (include source and compiled packages) are distributed under the open source MIT license as described below.
 
-Copyright 2012 Carl Bauer
+The MIT License
+Copyright © 2009 - 2013 Jonathan Hedley (jonathan@hedley.net)
 
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 
-    http://www.apache.org/licenses/LICENSE-2.0
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         
     

-

Ion

- https://github.com/koush/ion +

This software includes several Android classes from DashClock

+ https://code.google.com/p/dashclock/
         
-Copyright 2013 Koushik Dutta
+Copyright 2013 Google Inc.
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
-   http://www.apache.org/licenses/LICENSE-2.0
+ http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -142,17 +115,20 @@ limitations under the License.
     

-

subsampling-scale-image-view

- https://github.com/davemorrissey/subsampling-scale-image-view +

DragSortListView

+ https://github.com/bauerca/drag-sort-listview
         
-Copyright 2014 David Morrissey
+A subclass of the Android ListView component that enables drag
+and drop re-ordering of list items.
+
+Copyright 2012 Carl Bauer
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
-   http://www.apache.org/licenses/LICENSE-2.0
+    http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
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 f0b2f2da..d227da63 100644
--- a/Clover/app/src/main/java/org/floens/chan/ChanApplication.java
+++ b/Clover/app/src/main/java/org/floens/chan/ChanApplication.java
@@ -24,9 +24,6 @@ import android.view.ViewConfiguration;
 import com.android.volley.RequestQueue;
 import com.android.volley.toolbox.ImageLoader;
 import com.android.volley.toolbox.Volley;
-import com.koushikdutta.ion.Ion;
-import com.koushikdutta.ion.builder.Builders;
-import com.koushikdutta.ion.builder.LoadBuilder;
 
 import org.floens.chan.chan.ChanUrls;
 import org.floens.chan.core.manager.BoardManager;
@@ -104,10 +101,6 @@ public class ChanApplication extends Application {
         return fileCache;
     }
 
-    public static LoadBuilder getIon() {
-        return Ion.getDefault(getInstance()).build(getInstance());
-    }
-
     @Override
     public void onCreate() {
         super.onCreate();
@@ -135,8 +128,7 @@ public class ChanApplication extends Application {
 
         IconCache.createIcons(this);
 
-        Ion.getDefault(getInstance()).getCache().clear();
-        Ion.getDefault(getInstance()).getCache().setMaxSize(1 * 1024 * 1024);
+        cleanupOutdated();
 
         File cacheDir = getExternalCacheDir() != null ? getExternalCacheDir() : getCacheDir();
 
@@ -190,6 +182,23 @@ public class ChanApplication extends Application {
         foregroundChangedListeners.remove(listener);
     }
 
+    private void cleanupOutdated() {
+        File ionCacheFolder = new File(getCacheDir() + "/ion");
+        if (ionCacheFolder.exists() && ionCacheFolder.isDirectory()) {
+            Logger.i(TAG, "Clearing old ion folder");
+            for (File file : ionCacheFolder.listFiles()) {
+                if (!file.delete()) {
+                    Logger.i(TAG, "Could not delete old ion file " + file.getName());
+                }
+            }
+            if (!ionCacheFolder.delete()) {
+                Logger.i(TAG, "Could not delete old ion folder");
+            } else {
+                Logger.i(TAG, "Deleted old ion folder");
+            }
+        }
+    }
+
     public static interface ForegroundChangedListener {
         public void onForegroundChanged(boolean foreground);
     }
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 3ac78be8..2b640b7c 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 getCaptchaChallengeUrl() {
-        return scheme + "://www.google.com/recaptcha/api/challenge?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc";
-    }
-
-    public static String getCaptchaImageUrl(String challenge) {
-        return scheme + "://www.google.com/recaptcha/api2/payload?c=" + challenge;
+    public static String getCaptchaDomain() {
+        return scheme + "://www.google.com/";
     }
 
     public static String getCaptchaFallback() {
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 a8e9ed7f..299c462c 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
@@ -19,6 +19,7 @@ package org.floens.chan.core.manager;
 
 import android.content.Context;
 import android.content.Intent;
+import android.text.TextUtils;
 
 import org.floens.chan.ChanApplication;
 import org.floens.chan.R;
@@ -31,6 +32,7 @@ import org.floens.chan.utils.AndroidUtils;
 import org.floens.chan.utils.Logger;
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
 import org.jsoup.select.Elements;
 
 import java.io.File;
@@ -60,7 +62,6 @@ import ch.boye.httpclientandroidlib.util.EntityUtils;
 public class ReplyManager {
     private static final String TAG = "ReplyManager";
 
-    private static final Pattern challengePattern = Pattern.compile("challenge.?:.?'([\\w-]+)'");
     private static final Pattern responsePattern = Pattern.compile("");
     private static final int POST_TIMEOUT = 10000;
 
@@ -170,22 +171,6 @@ public class ReplyManager {
         public abstract void onFileLoading();
     }
 
-    /**
-     * Get the CAPTCHA challenge hash from an JSON response.
-     *
-     * @param total The total response from the server
-     * @return The pattern, or null when none was found.
-     */
-    public static String getChallenge(String total) {
-        Matcher matcher = challengePattern.matcher(total);
-
-        if (matcher.find() && matcher.groupCount() == 1) {
-            return matcher.group(1);
-        } else {
-            return null;
-        }
-    }
-
     public void sendPass(Pass pass, final PassListener listener) {
         Logger.i(TAG, "Sending pass login request");
 
@@ -336,6 +321,45 @@ public class ReplyManager {
         public String responseData = "";
     }
 
+    public void getCaptchaChallenge(final CaptchaChallengeListener listener) {
+        HttpPost httpPost = new HttpPost(ChanUrls.getCaptchaFallback());
+        httpPost.addHeader("User-Agent", "Android");
+
+        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 interface CaptchaChallengeListener {
+        public void onChallenge(String imageUrl, String challenge);
+
+        public void onError();
+    }
+
     private void getCaptchaHash(final CaptchaHashListener listener, String challenge, String response) {
         HttpPost httpPost = new HttpPost(ChanUrls.getCaptchaFallback());
 
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 9ff07d64..d886b923 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,21 +44,16 @@ import android.widget.TextView;
 import android.widget.Toast;
 import android.widget.ViewFlipper;
 
-import com.android.volley.Request.Method;
-import com.android.volley.Response;
-import com.android.volley.VolleyError;
 import com.android.volley.toolbox.NetworkImageView;
-import com.android.volley.toolbox.StringRequest;
 
 import org.floens.chan.ChanApplication;
 import org.floens.chan.R;
-import org.floens.chan.chan.ChanUrls;
-import org.floens.chan.core.settings.ChanSettings;
 import org.floens.chan.core.manager.ReplyManager;
 import org.floens.chan.core.manager.ReplyManager.ReplyResponse;
 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.core.settings.ChanSettings;
 import org.floens.chan.ui.animation.ViewFlipperAnimations;
 import org.floens.chan.ui.view.LoadView;
 import org.floens.chan.utils.AndroidUtils;
@@ -81,6 +76,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 = "";
 
@@ -210,8 +206,6 @@ public class ReplyFragment extends DialogFragment {
                 }
             });
             showCommentCount();
-
-            getCaptcha();
         } else {
             Logger.e(TAG, "Loadable in ReplyFragment was null");
             closeReply();
@@ -275,12 +269,6 @@ public class ReplyFragment extends DialogFragment {
         });
         captchaInput = (TextView) container.findViewById(R.id.reply_captcha);
 
-        if (ChanSettings.getPassEnabled()) {
-            ((TextView) container.findViewById(R.id.reply_captcha_text)).setText(R.string.pass_using);
-            container.findViewById(R.id.reply_captcha_container).setVisibility(View.GONE);
-            container.findViewById(R.id.reply_captcha).setVisibility(View.GONE);
-        }
-
         cancelButton = (Button) container.findViewById(R.id.reply_cancel);
         cancelButton.setOnClickListener(new OnClickListener() {
             @Override
@@ -320,11 +308,11 @@ public class ReplyFragment extends DialogFragment {
         submitButton.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View view) {
-                if (page == 0) {
-                    flipPage(1);
-                } else if (page == 1) {
+                if (page == 1 || ChanSettings.passLoggedIn()) {
                     flipPage(2);
                     submit();
+                } else {
+                    flipPage(1);
                 }
             }
         });
@@ -412,12 +400,11 @@ public class ReplyFragment extends DialogFragment {
         if (flipBack) {
             flipper.setInAnimation(ViewFlipperAnimations.BACK_IN);
             flipper.setOutAnimation(ViewFlipperAnimations.BACK_OUT);
-            flipper.showPrevious();
         } else {
             flipper.setInAnimation(ViewFlipperAnimations.NEXT_IN);
             flipper.setOutAnimation(ViewFlipperAnimations.NEXT_OUT);
-            flipper.showNext();
         }
+        flipper.setDisplayedChild(position);
 
         if (page == 0) {
             cancelButton.setText(R.string.cancel);
@@ -426,6 +413,11 @@ public class ReplyFragment extends DialogFragment {
         } else if (page == 2) {
             cancelButton.setText(R.string.close);
         }
+
+        if (page == 1 && !gotInitialCaptcha) {
+            gotInitialCaptcha = true;
+            getCaptcha();
+        }
     }
 
     /**
@@ -529,29 +521,22 @@ public class ReplyFragment extends DialogFragment {
         captchaContainer.setView(null);
         captchaInput.setText("");
 
-        String url = ChanUrls.getCaptchaChallengeUrl();
-
-        ChanApplication.getVolleyRequestQueue().add(new StringRequest(Method.GET, url, new Response.Listener() {
+        ChanApplication.getReplyManager().getCaptchaChallenge(new ReplyManager.CaptchaChallengeListener() {
             @Override
-            public void onResponse(String result) {
-                if (context != null) {
-                    String challenge = ReplyManager.getChallenge(result);
-                    if (challenge != null) {
-                        captchaChallenge = challenge;
-                        String imageUrl = ChanUrls.getCaptchaImageUrl(challenge);
+            public void onChallenge(String imageUrl, String challenge) {
+                gettingCaptcha = false;
 
-                        NetworkImageView captchaImage = new NetworkImageView(context);
-                        captchaImage.setImageUrl(imageUrl, ChanApplication.getVolleyImageLoader());
-                        captchaContainer.setView(captchaImage);
+                if (context != null) {
+                    captchaChallenge = challenge;
 
-                        gettingCaptcha = false;
-                    }
+                    NetworkImageView captchaImage = new NetworkImageView(context);
+                    captchaImage.setImageUrl(imageUrl, ChanApplication.getVolleyImageLoader());
+                    captchaContainer.setView(captchaImage);
                 }
             }
-        }, new Response.ErrorListener() {
+
             @Override
-            public void onErrorResponse(VolleyError error) {
-                error.printStackTrace();
+            public void onError() {
                 gettingCaptcha = false;
 
                 if (context != null) {
@@ -561,7 +546,7 @@ public class ReplyFragment extends DialogFragment {
                     captchaContainer.setView(text);
                 }
             }
-        }));
+        });
     }
 
     /**
@@ -622,7 +607,11 @@ public class ReplyFragment extends DialogFragment {
             submitButton.setEnabled(true);
             cancelButton.setEnabled(true);
             setClosable(true);
-            flipPage(1);
+            if (ChanSettings.passLoggedIn()) {
+                flipPage(0);
+            } else {
+                flipPage(1);
+            }
             getCaptcha();
             captchaInput.setText("");
         } else if (response.isSuccessful) {
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java
index 664cb2de..03ebfb78 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java
@@ -33,8 +33,6 @@ import android.widget.VideoView;
 
 import com.android.volley.VolleyError;
 import com.android.volley.toolbox.ImageLoader.ImageContainer;
-import com.koushikdutta.async.future.Future;
-import com.koushikdutta.ion.Response;
 
 import org.floens.chan.ChanApplication;
 import org.floens.chan.R;
@@ -46,6 +44,7 @@ import org.floens.chan.utils.Logger;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.concurrent.Future;
 
 import pl.droidsonroids.gif.GifDrawable;
 import pl.droidsonroids.gif.GifImageView;
@@ -62,12 +61,11 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
 
     private Mode mode = Mode.UNLOADED;
 
-    private boolean thumbnailNeeded = true;
     private boolean hasContent = false;
     private ImageContainer thumbnailRequest;
-    private Future> bigImageRequest;
-    private Future> gifRequest;
-    private Future> videoRequest;
+    private Future bigImageRequest;
+    private Future gifRequest;
+    private Future videoRequest;
 
     private VideoView videoView;
 
@@ -162,13 +160,6 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
         }, getWidth(), getHeight());
     }
 
-    private void cancelThumbnail() {
-        if (thumbnailRequest != null) {
-            thumbnailRequest.cancelRequest();
-            thumbnailRequest = null;
-        }
-    }
-
     public void setBigImage(String imageUrl) {
         if (getWidth() == 0 || getHeight() == 0) {
             Logger.e(TAG, "getWidth() or getHeight() returned 0, not loading big image");
@@ -176,12 +167,11 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
         }
 
         callback.setProgress(this, true);
-        bigImageRequest = ChanApplication.getFileCache().downloadFile(getContext(), imageUrl, new FileCache.DownloadedCallback() {
+        bigImageRequest = ChanApplication.getFileCache().downloadFile(imageUrl, new FileCache.DownloadedCallback() {
             @Override
             public void onProgress(long downloaded, long total, boolean done) {
                 if (done) {
 //                    callback.setLinearProgress(0, 0, true);
-                    thumbnailNeeded = false;
                 } else {
                     callback.setLinearProgress(MultiImageView.this, downloaded, total, false);
                 }
@@ -228,13 +218,6 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
         });
     }
 
-    private void cancelBigImage() {
-        if (bigImageRequest != null) {
-            bigImageRequest.cancel();
-            bigImageRequest = null;
-        }
-    }
-
     public void setGif(String gifUrl) {
         if (getWidth() == 0 || getHeight() == 0) {
             Logger.e(TAG, "getWidth() or getHeight() returned 0, not loading");
@@ -242,13 +225,12 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
         }
 
         callback.setProgress(this, true);
-        gifRequest = ChanApplication.getFileCache().downloadFile(getContext(), gifUrl, new FileCache.DownloadedCallback() {
+        gifRequest = ChanApplication.getFileCache().downloadFile(gifUrl, new FileCache.DownloadedCallback() {
             @Override
             public void onProgress(long downloaded, long total, boolean done) {
                 if (done) {
                     callback.setProgress(MultiImageView.this, false);
                     callback.setLinearProgress(MultiImageView.this, 0, 0, true);
-                    thumbnailNeeded = false;
                 } else {
                     callback.setLinearProgress(MultiImageView.this, downloaded, total, false);
                 }
@@ -295,13 +277,12 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
 
     public void setVideo(String videoUrl) {
         callback.setProgress(this, true);
-        videoRequest = ChanApplication.getFileCache().downloadFile(getContext(), videoUrl, new FileCache.DownloadedCallback() {
+        videoRequest = ChanApplication.getFileCache().downloadFile(videoUrl, new FileCache.DownloadedCallback() {
             @Override
             public void onProgress(long downloaded, long total, boolean done) {
                 if (done) {
                     callback.setProgress(MultiImageView.this, false);
                     callback.setLinearProgress(MultiImageView.this, 0, 0, true);
-                    thumbnailNeeded = false;
                 } else {
                     callback.setLinearProgress(MultiImageView.this, downloaded, total, false);
                 }
@@ -381,8 +362,8 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
     }
 
     public void onNotFoundError() {
-        Toast.makeText(getContext(), R.string.image_not_found, Toast.LENGTH_LONG).show();
         callback.setProgress(this, false);
+        Toast.makeText(getContext(), R.string.image_not_found, Toast.LENGTH_SHORT).show();
     }
 
     public void onOutOfMemoryError() {
@@ -392,13 +373,13 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
 
     public void cancelLoad() {
         if (bigImageRequest != null) {
-            bigImageRequest.cancel();
+            bigImageRequest.cancel(true);
         }
         if (gifRequest != null) {
-            gifRequest.cancel();
+            gifRequest.cancel(true);
         }
         if (videoRequest != null) {
-            videoRequest.cancel();
+            videoRequest.cancel(true);
         }
     }
 
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 67cc2f1c..9508512a 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
@@ -17,22 +17,34 @@
  */
 package org.floens.chan.utils;
 
-import android.content.Context;
 import android.util.Log;
 
-import com.koushikdutta.async.future.Future;
-import com.koushikdutta.async.future.FutureCallback;
-import com.koushikdutta.ion.ProgressCallback;
-import com.koushikdutta.ion.Response;
-
-import org.floens.chan.ChanApplication;
+import com.squareup.okhttp.Call;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
+import com.squareup.okhttp.internal.Util;
 
+import java.io.BufferedOutputStream;
+import java.io.Closeable;
 import java.io.File;
-import java.util.concurrent.CancellationException;
+import java.io.FileOutputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+
+import okio.BufferedSource;
 
 public class FileCache {
     private static final String TAG = "FileCache";
 
+    private static final ExecutorService executor = Executors.newFixedThreadPool(2);
+
+    private OkHttpClient httpClient;
+
     private final File directory;
     private final long maxSize;
 
@@ -42,6 +54,8 @@ public class FileCache {
         this.directory = directory;
         this.maxSize = maxSize;
 
+        httpClient = new OkHttpClient();
+
         makeDir();
         calculateSize();
     }
@@ -64,57 +78,16 @@ public class FileCache {
         return file.delete();
     }
 
-    public Future> downloadFile(Context context, String url, final DownloadedCallback callback) {
-        File file = get(url);
+    public Future downloadFile(final String urlString, final DownloadedCallback callback) {
+        File file = get(urlString);
         if (file.exists()) {
             file.setLastModified(Time.get());
             callback.onProgress(0, 0, true);
             callback.onSuccess(file);
             return null;
         } else {
-            return ChanApplication.getIon()
-                    .load(url)
-                    .progress(new ProgressCallback() {
-                        @Override
-                        public void onProgress(final long downloaded, final long total) {
-                            AndroidUtils.runOnUiThread(new Runnable() {
-                                @Override
-                                public void run() {
-                                    callback.onProgress(downloaded, total, false);
-                                }
-                            });
-                        }
-                    })
-                    .write(file)
-                    .withResponse()
-                    .setCallback(new FutureCallback>() {
-                        @Override
-                        public void onCompleted(Exception e, Response result) {
-                            callback.onProgress(0, 0, true);
-
-                            if (result != null && result.getHeaders() != null && result.getHeaders().code() / 100 != 2) {
-                                if (result.getResult() != null) {
-                                    delete(result.getResult());
-                                }
-                                callback.onFail(true);
-                                return;
-                            }
-
-                            if (e != null && !(e instanceof CancellationException)) {
-                                e.printStackTrace();
-                                if (result != null && result.getResult() != null) {
-                                    delete(result.getResult());
-                                }
-                                callback.onFail(false);
-                                return;
-                            }
-
-                            if (result != null && result.getResult() != null) {
-                                put(result.getResult());
-                                callback.onSuccess(result.getResult());
-                            }
-                        }
-                    });
+            FileCacheDownloader downloader = new FileCacheDownloader(this, urlString, file, callback);
+            return executor.submit(downloader);
         }
     }
 
@@ -180,4 +153,175 @@ public class FileCache {
 
         public void onFail(boolean notFound);
     }
+
+    private static class FileCacheDownloader implements Runnable {
+        private final FileCache fileCache;
+        private final String url;
+        private final File output;
+        private final DownloadedCallback callback;
+        private boolean cancelled = false;
+
+        private Closeable downloadInput;
+        private Closeable downloadOutput;
+        private Call call;
+        private ResponseBody body;
+
+        public FileCacheDownloader(FileCache fileCache, String url, File output, DownloadedCallback callback) {
+            this.fileCache = fileCache;
+            this.url = url;
+            this.output = output;
+            this.callback = callback;
+        }
+
+        public void run() {
+            try {
+                execute();
+            } catch (InterruptedIOException | InterruptedException e) {
+                cancelDueToCancellation(e);
+            } catch (Exception e) {
+                cancelDueToException(e);
+            } finally {
+                finish();
+            }
+        }
+
+        private void cancelDueToException(Exception e) {
+            if (cancelled) return;
+            cancelled = true;
+
+            Log.w(TAG, "IOException downloading file", e);
+
+            purgeOutput();
+
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onProgress(0, 0, true);
+                    callback.onFail(false);
+                }
+            });
+        }
+
+        private void cancelDueToHttpError(final int code) {
+            if (cancelled) return;
+            cancelled = true;
+
+            Log.w(TAG, "Cancel due to http error, code: " + code);
+
+            purgeOutput();
+
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onProgress(0, 0, true);
+                    callback.onFail(code == 404);
+                }
+            });
+        }
+
+        private void cancelDueToCancellation(Exception e) {
+            if (cancelled) return;
+            cancelled = true;
+
+            Log.d(TAG, "Cancel due to cancellation");
+
+            purgeOutput();
+
+            // No callback
+        }
+
+        private void success() {
+            fileCache.put(output);
+
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    callback.onProgress(0, 0, true);
+                    callback.onSuccess(output);
+                }
+            });
+            call = null;
+        }
+
+        private void finish() {
+            Util.closeQuietly(downloadInput);
+            Util.closeQuietly(downloadOutput);
+
+            if (call != null) {
+                call.cancel();
+                call = null;
+            }
+
+            if (body != null) {
+                Util.closeQuietly(body);
+                body = null;
+            }
+        }
+
+        private void purgeOutput() {
+            if (output.exists()) {
+                if (!output.delete()) {
+                    Log.w(TAG, "Could not delete the file in purgeOutput");
+                }
+            }
+        }
+
+        private long progressDownloaded;
+        private long progressTotal;
+        private boolean progressDone;
+        private final Runnable progressRunnable = new Runnable() {
+            @Override
+            public void run() {
+                callback.onProgress(progressDownloaded, progressTotal, progressDone);
+            }
+        };
+
+        private void progress(long downloaded, long total, boolean done) {
+            progressDownloaded = downloaded;
+            progressTotal = total;
+            progressDone = done;
+            post(progressRunnable);
+        }
+
+        private void post(Runnable runnable) {
+            AndroidUtils.runOnUiThread(runnable);
+        }
+
+        private void execute() throws Exception {
+            Request request = new Request.Builder().url(url).build();
+
+            call = fileCache.httpClient.newCall(request);
+            Response response = call.execute();
+            if (!response.isSuccessful()) {
+                cancelDueToHttpError(response.code());
+                return;
+            }
+
+            body = response.body();
+            long contentLength = body.contentLength();
+            BufferedSource source = body.source();
+            OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(output));
+
+            downloadInput = source;
+            downloadOutput = outputStream;
+
+            int read;
+            long total = 0;
+            long totalLast = 0;
+            byte[] buffer = new byte[4096];
+            while ((read = source.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, read);
+                total += read;
+
+                if (total >= totalLast + 16384) {
+                    totalLast = total;
+                    progress(total, contentLength, false);
+                }
+            }
+
+            if (Thread.currentThread().isInterrupted()) throw new InterruptedIOException();
+
+            success();
+        }
+    }
 }
diff --git a/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java b/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java
index 704c0fbc..ce0ad383 100644
--- a/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java
+++ b/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java
@@ -28,8 +28,6 @@ import android.media.MediaScannerConnection;
 import android.net.Uri;
 import android.widget.Toast;
 
-import com.koushikdutta.async.future.Future;
-
 import org.floens.chan.ChanApplication;
 import org.floens.chan.R;
 import org.floens.chan.core.settings.ChanSettings;
@@ -69,7 +67,7 @@ public class ImageSaver {
     }
 
     public void saveImage(final Context context, String imageUrl, final String name, final String extension, final boolean share) {
-        Future ionRequest = ChanApplication.getFileCache().downloadFile(context, imageUrl, new FileCache.DownloadedCallback() {
+        ChanApplication.getFileCache().downloadFile(imageUrl, new FileCache.DownloadedCallback() {
             @Override
             @SuppressWarnings("deprecation")
             public void onProgress(long downloaded, long total, boolean done) {
diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml
index 15d27b8b..c279b0e6 100644
--- a/Clover/app/src/main/res/values/strings.xml
+++ b/Clover/app/src/main/res/values/strings.xml
@@ -372,7 +372,6 @@ Don't have a 4chan Pass?<br>
     Connection error
     Using 4chan pass
     Off
-    Using 4chan pass
 
     %1$s images will be downloaded to %2$s
 
diff --git a/docs/gcaptcha.txt b/docs/gcaptcha.txt
index 590789ea..c435480b 100644
--- a/docs/gcaptcha.txt
+++ b/docs/gcaptcha.txt
@@ -7,25 +7,121 @@ https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLV
 Put this in an iframe and when the user has successfully filled in the captcha the user is told to copy paste the key into the real website field (outside the iframe, g-captcha-response)
 
 https://www.google.com/recaptcha/api/challenge?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc
-contains the key 'challenge'
-
-Now load the image
-https://www.google.com/recaptcha/api2/payload?c=CHALLENGE
+contains an image and and a field named "c" with the challenge
 
 after the user has solved the image, do a POST to
 
 https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc
 POST:
-c = CHALLENGE
-response = USER_RESPONSE
+c = the challenge given
+response = text response from the user
 
-You'll have to get hash inside the textarea of div.fbc-verification-token
+Next if the captcha was solved, get the hash inside the textarea of div.fbc-verification-token
+If the textarea isn't present, the captcha wasn't solved correctly you'll reuse the received html.
 
 next send off the reply to 4chan
 POST:
 g-captcha-response: HASH
 
 
+Initial HTML:
+
+
+
+    
+        
+        reCAPTCHA-uitdaging
+        
+    
+    
+        
+
+
+ +
+
+
+
+ +
+ +
+
+ +
+
+ reCAPTCHA-uitdagingsafbeelding +
+
+ +
+
+
+
+
+
+ + + +
+
+
+
+ + + +
+
+
+
+
+
+ +
+
+ + +Copy-paste code HTML: + + + + + reCAPTCHA-uitdaging + + + +
+
+
+ +
+
+
Kopieëer deze code
+
+ +
+
Deze code is 2 minuten geldig
+
+
+
Plak de code hier
+
+ +
+
+ +