diff --git a/Clover/app/src/main/AndroidManifest.xml b/Clover/app/src/main/AndroidManifest.xml
index 036db05b..5ebdb2af 100644
--- a/Clover/app/src/main/AndroidManifest.xml
+++ b/Clover/app/src/main/AndroidManifest.xml
@@ -37,6 +37,8 @@ along with this program. If not, see .
+
+
-
+
Loading captcha...
+
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 707006fd..d8ea649c 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.Context;
+import android.content.pm.PackageManager;
import android.os.StrictMode;
import android.view.ViewConfiguration;
@@ -40,6 +41,7 @@ import org.floens.chan.utils.Logger;
import java.io.File;
import java.lang.reflect.Field;
+import java.util.Locale;
import de.greenrobot.event.EventBus;
@@ -62,6 +64,7 @@ public class ChanApplication extends Application {
private static DatabaseManager databaseManager;
private static FileCache fileCache;
+ private String userAgent;
private int activityForegroundCounter = 0;
public ChanApplication() {
@@ -126,6 +129,16 @@ public class ChanApplication extends Application {
ChanUrls.loadScheme(ChanSettings.getNetworkHttps());
+ // User agent is /
+ String version = "";
+ try {
+ version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ version = version.toLowerCase(Locale.ENGLISH).replace(" ", "_");
+ userAgent = getString(R.string.app_name) + "/" + version;
+
IconCache.createIcons(this);
cleanupOutdated();
@@ -134,15 +147,18 @@ public class ChanApplication extends Application {
replyManager = new ReplyManager(this);
- volleyRequestQueue = Volley.newRequestQueue(this, replyManager.getUserAgent(), null, new File(cacheDir, Volley.DEFAULT_CACHE_DIR), VOLLEY_CACHE_SIZE);
+ volleyRequestQueue = Volley.newRequestQueue(this, 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);
+ fileCache = new FileCache(new File(cacheDir, FILE_CACHE_NAME), FILE_CACHE_DISK_SIZE, getUserAgent());
databaseManager = new DatabaseManager(this);
boardManager = new BoardManager();
watchManager = new WatchManager(this);
+ }
+ public String getUserAgent() {
+ return userAgent;
}
public void activityEnteredForeground() {
diff --git a/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java b/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java
index 9053f803..d2bf5c6f 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/manager/BoardManager.java
@@ -101,13 +101,6 @@ public class BoardManager {
}
private void storeBoards() {
- Logger.d(TAG, "Storing boards in database");
-
- for (Board test : allBoards) {
- if (test.saved) {
- Logger.d(TAG, "Board with value " + test.value + " saved");
- }
- }
updateByValueMap();
ChanApplication.getDatabaseManager().setBoards(allBoards);
@@ -130,8 +123,6 @@ public class BoardManager {
has = false;
for (int i = 0; i < allBoards.size(); i++) {
if (allBoards.get(i).value.equals(serverBoard.value)) {
- Logger.d(TAG, "Replaced board " + serverBoard.value + " with the server one");
-
Board old = allBoards.get(i);
serverBoard.id = old.id;
serverBoard.saved = old.saved;
@@ -162,7 +153,6 @@ public class BoardManager {
new BoardsRequest(ChanUrls.getBoardsUrl(), new Response.Listener>() {
@Override
public void onResponse(List data) {
- Logger.i(TAG, "Got boards from server");
setBoardsFromServer(data);
}
}, new Response.ErrorListener() {
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 644a228c..43d55887 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,7 +19,6 @@ package org.floens.chan.core.manager;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.PackageManager;
import android.text.TextUtils;
import com.squareup.okhttp.Callback;
@@ -63,7 +62,6 @@ public class ReplyManager {
private Reply draft;
private FileListener fileListener;
private final Random random = new Random();
- private String userAgent;
private OkHttpClient client;
public ReplyManager(Context context) {
@@ -74,21 +72,6 @@ public class ReplyManager {
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;
}
/**
@@ -475,7 +458,7 @@ public class ReplyManager {
}
private void makeOkHttpCall(Request.Builder requestBuilder, Callback callback) {
- requestBuilder.header("User-Agent", getUserAgent());
+ requestBuilder.header("User-Agent", ChanApplication.getInstance().getUserAgent());
Request request = requestBuilder.build();
client.newCall(request).enqueue(callback);
}
diff --git a/Clover/app/src/main/java/org/floens/chan/test/TestActivity.java b/Clover/app/src/main/java/org/floens/chan/test/TestActivity.java
new file mode 100644
index 00000000..136a8013
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/test/TestActivity.java
@@ -0,0 +1,304 @@
+/*
+ * 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.test;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+import com.android.volley.VolleyError;
+
+import org.floens.chan.ChanApplication;
+import org.floens.chan.core.loader.ChanLoader;
+import org.floens.chan.core.model.ChanThread;
+import org.floens.chan.core.model.Loadable;
+import org.floens.chan.core.model.Post;
+import org.floens.chan.utils.FileCache;
+import org.floens.chan.utils.Logger;
+import org.floens.chan.utils.ThemeHelper;
+
+import java.io.File;
+
+// Poor mans unit testing.
+// Move to proper unit testing when the gradle plugin fully supports it.
+public class TestActivity extends Activity implements View.OnClickListener {
+ private static final String TAG = "FileCacheTest";
+ private final Handler handler = new Handler(Looper.getMainLooper());
+
+ private Button clearCache;
+ private Button stats;
+ private Button simpleTest;
+ private Button cacheTest;
+ private Button timeoutTest;
+
+ private FileCache fileCache;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ ThemeHelper.getInstance().reloadPostViewColors(this);
+
+ LinearLayout linearLayout = new LinearLayout(this);
+ linearLayout.setOrientation(LinearLayout.VERTICAL);
+
+ clearCache = new Button(this);
+ clearCache.setText("Clear cache");
+ clearCache.setOnClickListener(this);
+ linearLayout.addView(clearCache);
+
+ stats = new Button(this);
+ stats.setText("Stats");
+ stats.setOnClickListener(this);
+ linearLayout.addView(stats);
+
+ simpleTest = new Button(this);
+ simpleTest.setText("Test download and cancel");
+ simpleTest.setOnClickListener(this);
+ linearLayout.addView(simpleTest);
+
+ cacheTest = new Button(this);
+ cacheTest.setText("Test cache size");
+ cacheTest.setOnClickListener(this);
+ linearLayout.addView(cacheTest);
+
+ timeoutTest = new Button(this);
+ timeoutTest.setText("Test multiple parallel");
+ timeoutTest.setOnClickListener(this);
+ linearLayout.addView(timeoutTest);
+
+ setContentView(linearLayout);
+
+ File cacheDir = getExternalCacheDir() != null ? getExternalCacheDir() : getCacheDir();
+ File fileCacheDir = new File(cacheDir, "filecache");
+ fileCache = new FileCache(fileCacheDir, 50 * 1024 * 1024, ChanApplication.getInstance().getUserAgent());
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == clearCache) {
+ clearCache();
+ } else if (v == stats) {
+ stats();
+ } else if (v == simpleTest) {
+ testDownloadAndCancel();
+ } else if (v == cacheTest) {
+ testCache();
+ } else if (v == timeoutTest) {
+ testTimeout();
+ }
+ }
+
+ public void clearCache() {
+ fileCache.clearCache();
+ }
+
+ public void stats() {
+ fileCache.logStats();
+ }
+
+ public void testDownloadAndCancel() {
+ // 1.9MB file of the clover Logger.i(TAG,
+ final String testImage = "http://a.pomf.se/ndbolc.png";
+ final File cacheFile = fileCache.get(testImage);
+
+ Logger.i(TAG, "Downloading " + testImage);
+ final FileCache.FileCacheDownloader downloader = fileCache.downloadFile(testImage, new FileCache.DownloadedCallback() {
+ @Override
+ public void onProgress(long downloaded, long total, boolean done) {
+ Logger.i(TAG, "onProgress " + downloaded + "/" + total + " " + done);
+ }
+
+ @Override
+ public void onSuccess(File file) {
+ Logger.i(TAG, "onSuccess " + file.exists());
+ }
+
+ @Override
+ public void onFail(boolean notFound) {
+ Logger.i(TAG, "onFail Cachefile exists() = " + cacheFile.exists());
+ }
+ });
+
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ fileCache.downloadFile(testImage, new FileCache.DownloadedCallback() {
+ @Override
+ public void onProgress(long downloaded, long total, boolean done) {
+ Logger.i(TAG, "2nd progress " + downloaded + "/" + total);
+ }
+
+ @Override
+ public void onSuccess(File file) {
+ Logger.i(TAG, "2nd onSuccess " + file.exists());
+ }
+
+ @Override
+ public void onFail(boolean notFound) {
+ Logger.i(TAG, "2nd onFail Cachefile exists() = " + cacheFile.exists());
+ }
+ });
+ }
+ }, 200);
+
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ Logger.i(TAG, "Cancelling download!");
+ downloader.cancel();
+ }
+ }, 500);
+
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ Logger.i(TAG, "File exists() = " + cacheFile.exists());
+ }
+ }, 600);
+
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ final File cache404File = fileCache.get(testImage + "404");
+ fileCache.downloadFile(testImage + "404", new FileCache.DownloadedCallback() {
+ @Override
+ public void onProgress(long downloaded, long total, boolean done) {
+ Logger.i(TAG, "404 progress " + downloaded + "/" + total + " " + done);
+ }
+
+ @Override
+ public void onSuccess(File file) {
+ Logger.i(TAG, "404 onSuccess " + file.exists());
+ }
+
+ @Override
+ public void onFail(boolean notFound) {
+ Logger.i(TAG, "404 onFail " + cache404File.exists());
+ }
+ });
+ }
+ }, 1000);
+ }
+
+ private void testCache() {
+ Loadable loadable = new Loadable("g");
+ loadable.mode = Loadable.Mode.CATALOG;
+ ChanLoader loader = new ChanLoader(loadable);
+ loader.addListener(new ChanLoader.ChanLoaderCallback() {
+ @Override
+ public void onChanLoaderData(ChanThread result) {
+ for (Post post : result.posts) {
+ if (post.hasImage) {
+ final String imageUrl = post.imageUrl;
+ fileCache.downloadFile(imageUrl, new FileCache.DownloadedCallback() {
+ @Override
+ public void onProgress(long downloaded, long total, boolean done) {
+ Logger.i(TAG, "Progress for " + imageUrl + " " + downloaded + "/" + total + " " + done);
+ }
+
+ @Override
+ public void onSuccess(File file) {
+ Logger.i(TAG, "onSuccess for " + imageUrl + " exists() = " + file.exists());
+ }
+
+ @Override
+ public void onFail(boolean notFound) {
+ Logger.i(TAG, "onFail for " + imageUrl);
+ }
+ });
+ }
+ }
+ }
+
+ @Override
+ public void onChanLoaderError(VolleyError error) {
+
+ }
+ });
+ loader.requestData();
+ }
+
+ private void testTimeout() {
+ testTimeoutInner("https://i.4cdn.org/hr/1429923649068.jpg", fileCache, 0);
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ testTimeoutInner("https://i.4cdn.org/hr/1430058524427.jpg", fileCache, 0);
+ }
+ }, 200);
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ testTimeoutInner("https://i.4cdn.org/hr/1430058627352.jpg", fileCache, 0);
+ }
+ }, 400);
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ testTimeoutInner("https://i.4cdn.org/hr/1430058580015.jpg", fileCache, 0);
+ }
+ }, 600);
+ }
+
+ private void testTimeoutInner(final String url, final FileCache fileCache, final int tries) {
+ final File cacheFile = fileCache.get(url);
+ Logger.i(TAG, "Downloading " + url + " try " + tries);
+ final FileCache.FileCacheDownloader downloader = fileCache.downloadFile(url, new FileCache.DownloadedCallback() {
+ @Override
+ public void onProgress(long downloaded, long total, boolean done) {
+ Logger.i(TAG, "onProgress " + url + " " + downloaded + "/" + total);
+ }
+
+ @Override
+ public void onSuccess(File file) {
+ Logger.i(TAG, "onSuccess " + file.exists());
+ }
+
+ @Override
+ public void onFail(boolean notFound) {
+ Logger.i(TAG, "onFail Cachefile exists() = " + cacheFile.exists());
+ }
+ });
+
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (downloader == null) {
+ Logger.i(TAG, "Downloader null, cannot cancel");
+ } else {
+ downloader.cancel();
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (tries < 10) {
+ testTimeoutInner(url, fileCache, tries + 1);
+ } else {
+ fileCache.logStats();
+ }
+ }
+ }, 500);
+ }
+ }
+ }, 1000);
+ }
+}
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 3e059d79..f9892cb7 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
@@ -204,7 +204,7 @@ public class ReplyFragment extends DialogFragment implements CaptchaLayout.Captc
String baseUrl = loadable.isThreadMode() ? ChanUrls.getThreadUrlDesktop(loadable.board, loadable.no) : ChanUrls.getBoardUrlDesktop(loadable.board);
captchaLayout.initCaptcha(baseUrl, ChanUrls.getCaptchaSiteKey(),
- ThemeHelper.getInstance().getTheme().isLightTheme, ChanApplication.getReplyManager().getUserAgent(), this);
+ ThemeHelper.getInstance().getTheme().isLightTheme, ChanApplication.getInstance().getUserAgent(), this);
} else {
Logger.e(TAG, "Loadable in ReplyFragment was null");
closeReply();
@@ -581,8 +581,8 @@ public class ReplyFragment extends DialogFragment implements CaptchaLayout.Captc
if (ChanSettings.passLoggedIn()) {
flipPage(0);
} else {
- captchaLayout.reset();
flipPage(1);
+ captchaLayout.reset();
}
} else if (response.isSuccessful) {
shouldSaveDraft = false;
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
index 63777362..a681ca13 100644
--- 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
@@ -21,7 +21,10 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.text.TextUtils;
import android.util.AttributeSet;
+import android.util.Log;
+import android.webkit.ConsoleMessage;
import android.webkit.JavascriptInterface;
+import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
@@ -29,6 +32,8 @@ import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils;
public class CaptchaLayout extends WebView {
+ private static final String TAG = "CaptchaLayout";
+
private CaptchaCallback callback;
private boolean loaded = false;
private String baseUrl;
@@ -58,6 +63,15 @@ public class CaptchaLayout extends WebView {
settings.setJavaScriptEnabled(true);
settings.setUserAgentString(userAgent);
+ setWebChromeClient(new WebChromeClient() {
+ @Override
+ public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
+ Log.i(TAG, consoleMessage.lineNumber() + ":" + consoleMessage.message() + " " + consoleMessage.sourceId());
+ return true;
+ }
+ });
+ setBackgroundColor(0x00000000);
+
addJavascriptInterface(new CaptchaInterface(this), "CaptchaCallback");
}
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 382431a5..9e7ae5ca 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
@@ -45,7 +45,6 @@ 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;
@@ -64,9 +63,9 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
private boolean hasContent = false;
private ImageContainer thumbnailRequest;
- private Future bigImageRequest;
- private Future gifRequest;
- private Future videoRequest;
+ private FileCache.FileCacheDownloader bigImageRequest;
+ private FileCache.FileCacheDownloader gifRequest;
+ private FileCache.FileCacheDownloader videoRequest;
private VideoView videoView;
private boolean videoError = false;
@@ -398,13 +397,13 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
thumbnailRequest.cancelRequest();
}
if (bigImageRequest != null) {
- bigImageRequest.cancel(true);
+ bigImageRequest.cancel();
}
if (gifRequest != null) {
- gifRequest.cancel(true);
+ gifRequest.cancel();
}
if (videoRequest != null) {
- videoRequest.cancel(true);
+ videoRequest.cancel();
}
}
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 0ed17843..0fc17225 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,83 +17,152 @@
*/
package org.floens.chan.utils;
-import android.util.Log;
+import android.os.Handler;
+import android.os.Looper;
import com.squareup.okhttp.Call;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
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;
import java.io.FileOutputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import okio.BufferedSource;
public class FileCache {
private static final String TAG = "FileCache";
+ private static final int TIMEOUT = 10000;
+ private static final int TRIM_TRIES = 20;
+ private static final int THREAD_COUNT = 2;
- private static final ExecutorService executor = Executors.newFixedThreadPool(2);
+ private static final ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
+ private String userAgent;
private OkHttpClient httpClient;
- private static String userAgent;
private final File directory;
private final long maxSize;
-
private long size;
- public FileCache(File directory, long maxSize) {
+ private List downloaders = new ArrayList<>();
+
+ public FileCache(File directory, long maxSize, String userAgent) {
this.directory = directory;
this.maxSize = maxSize;
+ this.userAgent = userAgent;
httpClient = new OkHttpClient();
- userAgent = ChanApplication.getReplyManager().getUserAgent();
+ httpClient.setConnectTimeout(TIMEOUT, TimeUnit.MILLISECONDS);
+ httpClient.setReadTimeout(TIMEOUT, TimeUnit.MILLISECONDS);
+ httpClient.setWriteTimeout(TIMEOUT, TimeUnit.MILLISECONDS);
+
+ // Disable SPDY, causes reproducable timeouts, only one download at the same time and other fun stuff
+ httpClient.setProtocols(Collections.singletonList(Protocol.HTTP_1_1));
makeDir();
calculateSize();
}
+ public void logStats() {
+ Logger.i(TAG, "Cache size = " + size + "/" + maxSize);
+ Logger.i(TAG, "downloaders.size() = " + downloaders.size());
+ for (FileCacheDownloader downloader : downloaders) {
+ Logger.i(TAG, "url = " + downloader.getUrl() + " cancelled = " + downloader.cancelled);
+ }
+ }
+
+ public void clearCache() {
+ Logger.d(TAG, "Clearing cache");
+ for (FileCacheDownloader downloader : downloaders) {
+ downloader.cancel();
+ }
+
+ if (directory.exists() && directory.isDirectory()) {
+ for (File file : directory.listFiles()) {
+ if (!file.delete()) {
+ Logger.d(TAG, "Could not delete cache file while clearing cache " + file.getName());
+ }
+ }
+ }
+ calculateSize();
+ }
+
+ /**
+ * Start downloading the file located at the url.
+ * If the file is in the cache then the callback is executed immediately and null is returned.
+ * Otherwise if the file is downloading or has not yet started downloading an {@link FileCacheDownloader} is returned.
+ * Only call this method on the UI thread.
+ *
+ * @param urlString the url to download.
+ * @param callback callback to execute callbacks on.
+ * @return null if in the cache, {@link FileCacheDownloader} otherwise.
+ */
+ public FileCacheDownloader downloadFile(final String urlString, final DownloadedCallback callback) {
+ FileCacheDownloader downloader = null;
+ for (FileCacheDownloader downloaderItem : downloaders) {
+ if (downloaderItem.getUrl().equals(urlString)) {
+ downloader = downloaderItem;
+ break;
+ }
+ }
+
+ if (downloader != null) {
+ downloader.addCallback(callback);
+ return downloader;
+ } else {
+ File file = get(urlString);
+ if (file.exists()) {
+ // TODO: setLastModified doesn't seem to work on Android...
+ if (!file.setLastModified(Time.get())) {
+// Logger.e(TAG, "Could not set last modified time on file");
+ }
+ callback.onProgress(0, 0, true);
+ callback.onSuccess(file);
+ return null;
+ } else {
+ FileCacheDownloader newDownloader = new FileCacheDownloader(this, urlString, file, userAgent);
+ newDownloader.addCallback(callback);
+ Future> future = executor.submit(newDownloader);
+ newDownloader.setFuture(future);
+ downloaders.add(newDownloader);
+ return newDownloader;
+ }
+ }
+ }
+
public File get(String key) {
makeDir();
return new File(directory, Integer.toString(key.hashCode()));
}
- public void put(File file) {
+ private void put(File file) {
size += file.length();
trim();
}
- public boolean delete(File file) {
+ private boolean delete(File file) {
size -= file.length();
return file.delete();
}
- 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 {
- FileCacheDownloader downloader = new FileCacheDownloader(this, urlString, file, callback);
- return executor.submit(downloader);
- }
- }
-
private void makeDir() {
if (!directory.exists()) {
if (!directory.mkdirs()) {
@@ -106,9 +175,9 @@ public class FileCache {
private void trim() {
int tries = 0;
- while (size > maxSize && tries++ < 10) {
+ while (size > maxSize && tries++ < TRIM_TRIES) {
File[] files = directory.listFiles();
- if (files == null) {
+ if (files == null || files.length <= 1) {
break;
}
long age = Long.MAX_VALUE;
@@ -123,12 +192,12 @@ public class FileCache {
}
if (oldest == null) {
- Log.e(TAG, "No files to trim");
+ Logger.e(TAG, "No files to trim");
break;
} else {
- Log.d(TAG, "Deleting " + oldest.getAbsolutePath());
+ Logger.d(TAG, "Deleting " + oldest.getAbsolutePath());
if (!delete(oldest)) {
- Log.e(TAG, "Cannot delete cache file");
+ Logger.e(TAG, "Cannot delete cache file while trimming");
calculateSize();
break;
}
@@ -149,6 +218,10 @@ public class FileCache {
}
}
+ private void removeFromDownloaders(FileCacheDownloader downloader) {
+ downloaders.remove(downloader);
+ }
+
public interface DownloadedCallback {
void onProgress(long downloaded, long total, boolean done);
@@ -157,50 +230,88 @@ public class FileCache {
void onFail(boolean notFound);
}
- private static class FileCacheDownloader implements Runnable {
+ public 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 final String userAgent;
+
+ // Modify the callbacks list on the UI thread only!
+ private final List callbacks = new ArrayList<>();
+
+ private AtomicBoolean running = new AtomicBoolean(false);
+ private AtomicBoolean userCancelled = new AtomicBoolean(false);
private Closeable downloadInput;
private Closeable downloadOutput;
private Call call;
private ResponseBody body;
+ private boolean cancelled = false;
+ private Future> future;
- public FileCacheDownloader(FileCache fileCache, String url, File output, DownloadedCallback callback) {
+ private FileCacheDownloader(FileCache fileCache, String url, File output, String userAgent) {
this.fileCache = fileCache;
this.url = url;
this.output = output;
- this.callback = callback;
+ this.userAgent = userAgent;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void addCallback(DownloadedCallback callback) {
+ callbacks.add(callback);
+ }
+
+ /**
+ * Cancel this download by interrupting the downloading thread. No callbacks will be executed.
+ */
+ public void cancel() {
+ if (userCancelled.compareAndSet(false, true)) {
+ future.cancel(true);
+ // Did not start running yet, call cancelDueToCancellation manually to remove from downloaders list.
+ if (!running.get()) {
+ cancelDueToCancellation();
+ }
+ }
}
public void run() {
+ Logger.d(TAG, "Start load of " + url);
try {
+ running.set(true);
execute();
- } catch (InterruptedIOException | InterruptedException e) {
- cancelDueToCancellation(e);
} catch (Exception e) {
- cancelDueToException(e);
+ if (userCancelled.get()) {
+ cancelDueToCancellation();
+ } else {
+ cancelDueToException(e);
+ }
} finally {
- finish();
+ cleanup();
}
}
+ private void setFuture(Future> future) {
+ this.future = future;
+ }
+
private void cancelDueToException(Exception e) {
if (cancelled) return;
cancelled = true;
- Log.w(TAG, "IOException downloading file", e);
-
- purgeOutput();
+ Logger.w(TAG, "IOException downloading url " + url, e);
post(new Runnable() {
@Override
public void run() {
- callback.onProgress(0, 0, true);
- callback.onFail(false);
+ purgeOutput();
+ removeFromDownloadersList();
+ for (DownloadedCallback callback : callbacks) {
+ callback.onProgress(0, 0, true);
+ callback.onFail(false);
+ }
}
});
}
@@ -209,44 +320,57 @@ public class FileCache {
if (cancelled) return;
cancelled = true;
- Log.w(TAG, "Cancel due to http error, code: " + code);
-
- purgeOutput();
+ Logger.w(TAG, "Cancel " + url + " due to http error, code: " + code);
post(new Runnable() {
@Override
public void run() {
- callback.onProgress(0, 0, true);
- callback.onFail(code == 404);
+ purgeOutput();
+ removeFromDownloadersList();
+ for (DownloadedCallback callback : callbacks) {
+ callback.onProgress(0, 0, true);
+ callback.onFail(code == 404);
+ }
}
});
}
- private void cancelDueToCancellation(Exception e) {
+ private void cancelDueToCancellation() {
if (cancelled) return;
cancelled = true;
- Log.d(TAG, "Cancel due to cancellation");
-
- purgeOutput();
+ Logger.d(TAG, "Cancel " + url + " due to cancellation");
- // No callback
+ post(new Runnable() {
+ @Override
+ public void run() {
+ purgeOutput();
+ removeFromDownloadersList();
+ }
+ });
}
private void success() {
- fileCache.put(output);
+ Logger.d(TAG, "Success downloading " + url);
post(new Runnable() {
@Override
public void run() {
- callback.onProgress(0, 0, true);
- callback.onSuccess(output);
+ fileCache.put(output);
+ removeFromDownloadersList();
+ for (DownloadedCallback callback : callbacks) {
+ callback.onProgress(0, 0, true);
+ callback.onSuccess(output);
+ }
}
});
call = null;
}
- private void finish() {
+ /**
+ * Always called before any cancelDueTo method or success on the downloading thread.
+ */
+ private void cleanup() {
Util.closeQuietly(downloadInput);
Util.closeQuietly(downloadOutput);
@@ -261,29 +385,27 @@ public class FileCache {
}
}
+ private void removeFromDownloadersList() {
+ fileCache.removeFromDownloaders(this);
+ }
+
private void purgeOutput() {
if (output.exists()) {
if (!output.delete()) {
- Log.w(TAG, "Could not delete the file in purgeOutput");
+ Logger.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 postProgress(final long downloaded, final long total, final boolean done) {
+ post(new Runnable() {
+ @Override
+ public void run() {
+ for (DownloadedCallback callback : callbacks) {
+ callback.onProgress(downloaded, total, done);
+ }
+ }
+ });
}
private void post(Runnable runnable) {
@@ -293,7 +415,7 @@ public class FileCache {
private void execute() throws Exception {
Request request = new Request.Builder()
.url(url)
- .header("User-Agent", FileCache.userAgent)
+ .header("User-Agent", userAgent)
.build();
call = fileCache.httpClient.newCall(request);
@@ -311,6 +433,8 @@ public class FileCache {
downloadInput = source;
downloadOutput = outputStream;
+ Logger.d(TAG, "Got input stream for " + url);
+
int read;
long total = 0;
long totalLast = 0;
@@ -321,8 +445,10 @@ public class FileCache {
if (total >= totalLast + 16384) {
totalLast = total;
- progress(total, contentLength, false);
+ postProgress(total, contentLength <= 0 ? total : contentLength, false);
}
+
+ if (Thread.currentThread().isInterrupted()) throw new InterruptedIOException();
}
if (Thread.currentThread().isInterrupted()) throw new InterruptedIOException();