diff --git a/Clover/app/src/main/java/org/floens/chan/core/cache/CacheHandler.java b/Clover/app/src/main/java/org/floens/chan/core/cache/CacheHandler.java
new file mode 100644
index 00000000..4095ee45
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/core/cache/CacheHandler.java
@@ -0,0 +1,175 @@
+/*
+ * 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.cache;
+
+import android.support.annotation.AnyThread;
+import android.support.annotation.MainThread;
+import android.support.annotation.WorkerThread;
+import android.util.Pair;
+
+import org.floens.chan.utils.Logger;
+
+import java.io.File;
+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.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class CacheHandler {
+ private static final String TAG = "CacheHandler";
+ private static final int TRIM_TRIES = 20;
+
+ private final ExecutorService pool = Executors.newFixedThreadPool(1);
+
+ private final File directory;
+ private final long maxSize;
+
+ /**
+ * An estimation of the current size of the directory. Used to check if trim must be run
+ * because the folder exceeds the maximum size.
+ */
+ private AtomicLong size = new AtomicLong();
+ private AtomicBoolean trimRunning = new AtomicBoolean(false);
+
+ public CacheHandler(File directory, long maxSize) {
+ this.directory = directory;
+ this.maxSize = maxSize;
+
+ createDirectories();
+ backgroundRecalculateSize();
+ }
+
+ @MainThread
+ public boolean exists(String key) {
+ return get(key).exists();
+ }
+
+ @MainThread
+ public File get(String key) {
+ createDirectories();
+
+ return new File(directory, hash(key));
+ }
+
+ @MainThread
+ protected void fileWasAdded(File file) {
+ long adjustedSize = size.addAndGet(file.length());
+
+ if (adjustedSize > maxSize && trimRunning.compareAndSet(false, true)) {
+ pool.submit(() -> {
+ try {
+ trim();
+ } catch (Exception e) {
+ Logger.e(TAG, "Error trimming", e);
+ } finally {
+ trimRunning.set(false);
+ }
+ });
+ }
+ }
+
+ @MainThread
+ public void clearCache() {
+ Logger.d(TAG, "Clearing cache");
+
+ 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());
+ }
+ }
+ }
+
+ recalculateSize();
+ }
+
+ @MainThread
+ public void createDirectories() {
+ if (!directory.exists()) {
+ if (!directory.mkdirs()) {
+ Logger.e(TAG, "Unable to create file cache dir " +
+ directory.getAbsolutePath());
+ }
+ }
+ }
+
+ @MainThread
+ private void backgroundRecalculateSize() {
+ pool.submit(this::recalculateSize);
+ }
+
+ @AnyThread
+ private void recalculateSize() {
+ long calculatedSize = 0;
+
+ File[] files = directory.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ calculatedSize += file.length();
+ }
+ }
+
+ size.set(calculatedSize);
+ }
+
+ @WorkerThread
+ private void trim() {
+ File[] directoryFiles = directory.listFiles();
+
+ // Don't try to trim empty directories or just one file in it.
+ if (directoryFiles == null || directoryFiles.length <= 1) {
+ return;
+ }
+
+ // Get all files with their last modified times.
+ List> files = new ArrayList<>(directoryFiles.length);
+ for (File file : directoryFiles) {
+ files.add(new Pair<>(file, file.lastModified()));
+ }
+
+ // Sort by oldest first.
+ Collections.sort(files, (o1, o2) -> Long.signum(o1.second - o2.second));
+
+ // Trim as long as the directory size exceeds the threshold and we haven't reached
+ // the trim limit.
+ long workingSize = size.get();
+ for (int i = 0; workingSize >= maxSize && i < Math.min(files.size(), TRIM_TRIES); i++) {
+ File file = files.get(i).first;
+
+ Logger.d(TAG, "Delete for trim " + file.getAbsolutePath());
+ workingSize -= file.length();
+
+ boolean deleteResult = file.delete();
+
+ if (!deleteResult) {
+ Logger.e(TAG, "Failed to delete cache file for trim");
+ }
+ }
+
+ recalculateSize();
+ }
+
+ @AnyThread
+ private String hash(String key) {
+ return String.valueOf(key.hashCode());
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/cache/FileCache.java b/Clover/app/src/main/java/org/floens/chan/core/cache/FileCache.java
index 541b4dbc..c6c3b814 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/cache/FileCache.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/cache/FileCache.java
@@ -17,57 +17,36 @@
*/
package org.floens.chan.core.cache;
-import org.floens.chan.core.settings.ChanSettings;
-import org.floens.chan.utils.AndroidUtils;
+import android.support.annotation.MainThread;
+
import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Time;
-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.Arrays;
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 java.util.concurrent.atomic.AtomicLong;
-import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
-import okhttp3.Request;
-import okhttp3.Response;
-import okhttp3.ResponseBody;
-import okhttp3.internal.Util;
-import okio.BufferedSource;
-public class FileCache {
+public class FileCache implements FileCacheDownloader.Callback {
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 int DOWNLOAD_POOL_SIZE = 2;
- private static final ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT);
+ private final ExecutorService downloadPool = Executors.newFixedThreadPool(DOWNLOAD_POOL_SIZE);
private String userAgent;
- private OkHttpClient httpClient;
+ protected OkHttpClient httpClient;
- private final File directory;
- private final long maxSize;
- private AtomicLong size = new AtomicLong();
- private AtomicBoolean trimRunning = new AtomicBoolean(false);
+ private final CacheHandler cacheHandler;
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.Builder()
@@ -78,411 +57,88 @@ public class FileCache {
.protocols(Collections.singletonList(Protocol.HTTP_1_1))
.build();
- createDirectories();
- recalculateSize();
+ cacheHandler = new CacheHandler(directory, maxSize);
}
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());
- }
- }
- }
- recalculateSize();
+ cacheHandler.clearCache();
}
/**
* 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.
+ * 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 a
+ * {@link FileCacheDownloader} is returned.
*
- * @param urlString the url to download.
- * @param callback callback to execute callbacks on.
- * @return null if in the cache, {@link FileCacheDownloader} otherwise.
+ * @param url the url to download.
+ * @param listener listener to execute callbacks on.
+ * @return {@code 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;
+ @MainThread
+ public FileCacheDownloader downloadFile(String url, FileCacheListener listener) {
+ FileCacheDownloader runningDownloaderForKey = getDownloaderByKey(url);
+ if (runningDownloaderForKey != null) {
+ runningDownloaderForKey.addListener(listener);
+ return runningDownloaderForKey;
+ }
+
+ File file = get(url);
+ if (file.exists()) {
+ handleFileImmediatelyAvailable(listener, file);
+ return null;
} 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;
- }
+ return handleStartDownload(listener, file, url);
}
}
- public boolean exists(String key) {
- return get(key).exists();
- }
-
- public File get(String key) {
- createDirectories();
-
- return new File(directory, Integer.toString(key.hashCode()));
- }
-
- private void createDirectories() {
- if (!directory.exists()) {
- if (!directory.mkdirs()) {
- Logger.e(TAG, "Unable to create file cache dir " + directory.getAbsolutePath());
- } else {
- recalculateSize();
+ public FileCacheDownloader getDownloaderByKey(String key) {
+ for (FileCacheDownloader downloader : downloaders) {
+ if (downloader.getUrl().equals(key)) {
+ return downloader;
}
}
+ return null;
}
- private void fileWasAdded(File file) {
- long adjustedSize = size.addAndGet(file.length());
-
- if (adjustedSize > maxSize && trimRunning.compareAndSet(false, true)) {
- executor.submit(new Runnable() {
- @Override
- public void run() {
- try {
- trim();
- } catch (Exception e) {
- Logger.e(TAG, "Error trimming", e);
- } finally {
- trimRunning.set(false);
- }
- }
- });
- }
- }
-
- // Called on a background thread
- private void trim() {
- File[] directoryFiles = directory.listFiles();
-
- // Don't try to trim empty directories or just one image in it.
- if (directoryFiles == null || directoryFiles.length <= 1) {
- return;
- }
-
- List files = new ArrayList<>(Arrays.asList(directoryFiles));
-
- int trimmed = 0;
- long workingSize = size.get();
- int tries = 0;
- while (workingSize > maxSize && tries++ < TRIM_TRIES) {
- // Find the oldest file
- long oldest = Long.MAX_VALUE;
- File oldestFile = null;
- for (File file : files) {
- long modified = file.lastModified();
- if (modified != 0L && modified < oldest) {
- oldest = modified;
- oldestFile = file;
- }
- }
-
- if (oldestFile != null) {
- Logger.d(TAG, "Delete for trim" + oldestFile.getAbsolutePath());
- workingSize -= oldestFile.length();
- trimmed++;
- files.remove(oldestFile);
-
- if (!oldestFile.delete()) {
- Logger.e(TAG, "Failed to delete cache file for trim");
- break;
- }
- } else {
- Logger.e(TAG, "No files to trim");
- break;
- }
- }
-
- if (trimmed > 0) {
- recalculateSize();
- }
+ @Override
+ public void downloaderFinished(FileCacheDownloader fileCacheDownloader) {
+ downloaders.remove(fileCacheDownloader);
}
- // Called on a background thread
- private void recalculateSize() {
- long calculatedSize = 0;
-
- File[] files = directory.listFiles();
- if (files != null) {
- for (File file : files) {
- calculatedSize += file.length();
- }
- }
-
- size.set(calculatedSize);
+ @Override
+ public void downloaderAddedFile(File file) {
+ cacheHandler.fileWasAdded(file);
}
- private void removeFromDownloaders(FileCacheDownloader downloader) {
- downloaders.remove(downloader);
+ public boolean exists(String key) {
+ return cacheHandler.exists(key);
}
- public interface DownloadedCallback {
- void onProgress(long downloaded, long total, boolean done);
-
- void onSuccess(File file);
-
- void onFail(boolean notFound);
+ public File get(String key) {
+ return cacheHandler.get(key);
}
- public static class FileCacheDownloader implements Runnable {
- private final FileCache fileCache;
- private final String url;
- private final File output;
- 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;
-
- private FileCacheDownloader(FileCache fileCache, String url, File output, String userAgent) {
- this.fileCache = fileCache;
- this.url = url;
- this.output = output;
- this.userAgent = userAgent;
- }
-
- public String getUrl() {
- return url;
+ private void handleFileImmediatelyAvailable(FileCacheListener listener, File file) {
+ // 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");
}
+ listener.onSuccess(file);
+ listener.onEnd();
+ }
- 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 (Exception e) {
- if (userCancelled.get()) {
- cancelDueToCancellation();
- } else {
- cancelDueToException(e);
- }
- } finally {
- cleanup();
- }
- }
-
- public Future> getFuture() {
- return future;
- }
-
- private void setFuture(Future> future) {
- this.future = future;
- }
-
- private void cancelDueToException(Exception e) {
- if (cancelled) return;
- cancelled = true;
-
- Logger.w(TAG, "IOException downloading url " + url, e);
-
- post(new Runnable() {
- @Override
- public void run() {
- purgeOutput();
- removeFromDownloadersList();
- for (DownloadedCallback callback : callbacks) {
- callback.onProgress(0, 0, true);
- callback.onFail(false);
- }
- }
- });
- }
-
- private void cancelDueToHttpError(final int code) {
- if (cancelled) return;
- cancelled = true;
-
- Logger.w(TAG, "Cancel " + url + " due to http error, code: " + code);
-
- post(new Runnable() {
- @Override
- public void run() {
- purgeOutput();
- removeFromDownloadersList();
- for (DownloadedCallback callback : callbacks) {
- callback.onProgress(0, 0, true);
- callback.onFail(code == 404);
- }
- }
- });
- }
-
- private void cancelDueToCancellation() {
- if (cancelled) return;
- cancelled = true;
-
- Logger.d(TAG, "Cancel " + url + " due to cancellation");
-
- post(new Runnable() {
- @Override
- public void run() {
- purgeOutput();
- removeFromDownloadersList();
- }
- });
- }
-
- private void success() {
- Logger.d(TAG, "Success downloading " + url);
-
- post(new Runnable() {
- @Override
- public void run() {
- fileCache.fileWasAdded(output);
- removeFromDownloadersList();
- for (DownloadedCallback callback : callbacks) {
- callback.onProgress(0, 0, true);
- callback.onSuccess(output);
- }
- }
- });
- call = null;
- }
-
- /**
- * Always called before any cancelDueTo method or success on the downloading thread.
- */
- private void cleanup() {
- Util.closeQuietly(downloadInput);
- Util.closeQuietly(downloadOutput);
-
- if (call != null) {
- call.cancel();
- call = null;
- }
-
- if (body != null) {
- Util.closeQuietly(body);
- body = null;
- }
- }
-
- private void removeFromDownloadersList() {
- fileCache.removeFromDownloaders(this);
- }
-
- private void purgeOutput() {
- if (output.exists()) {
- if (!output.delete()) {
- Logger.w(TAG, "Could not delete the file in purgeOutput");
- }
- }
- }
-
- 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) {
- AndroidUtils.runOnUiThread(runnable);
- }
-
- private void execute() throws Exception {
- Request request = new Request.Builder()
- .url(url)
- .header("User-Agent", userAgent)
- .build();
-
- call = fileCache.httpClient.newBuilder()
- .proxy(ChanSettings.getProxy())
- .build()
- .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;
-
- Logger.d(TAG, "Got input stream for " + url);
-
- 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;
- postProgress(total, contentLength <= 0 ? total : contentLength, false);
- }
- }
-
- if (Thread.currentThread().isInterrupted()) throw new InterruptedIOException();
-
- success();
- }
+ private FileCacheDownloader handleStartDownload(
+ FileCacheListener listener, File file, String url) {
+ FileCacheDownloader downloader = FileCacheDownloader.fromCallbackClientUrlOutputUserAgent(
+ this, httpClient, url, file, userAgent);
+ downloader.addListener(listener);
+ downloader.execute(downloadPool);
+ downloaders.add(downloader);
+ return downloader;
}
}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/cache/FileCacheDownloader.java b/Clover/app/src/main/java/org/floens/chan/core/cache/FileCacheDownloader.java
new file mode 100644
index 00000000..64baafc7
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/core/cache/FileCacheDownloader.java
@@ -0,0 +1,331 @@
+/*
+ * 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.cache;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.AnyThread;
+import android.support.annotation.MainThread;
+import android.support.annotation.WorkerThread;
+
+import org.floens.chan.core.settings.ChanSettings;
+import org.floens.chan.utils.Logger;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import okhttp3.Call;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+import okhttp3.internal.Util;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.Okio;
+import okio.Source;
+
+public class FileCacheDownloader implements Runnable {
+ private static final String TAG = "FileCacheDownloader";
+ private static final long BUFFER_SIZE = 8192;
+ private static final long NOTIFY_SIZE = BUFFER_SIZE * 8;
+
+ private final OkHttpClient httpClient;
+ private final String url;
+ private final File output;
+ private final String userAgent;
+ private final Handler handler;
+
+ // Main thread only.
+ private final Callback callback;
+ private final List listeners = new ArrayList<>();
+
+ // Main and worker thread.
+ private AtomicBoolean running = new AtomicBoolean(false);
+ private AtomicBoolean cancel = new AtomicBoolean(false);
+ private Future> future;
+
+ // Worker thread.
+ private Call call;
+ private ResponseBody body;
+
+ static FileCacheDownloader fromCallbackClientUrlOutputUserAgent(
+ Callback callback, OkHttpClient httpClient, String url,
+ File output, String userAgent) {
+ return new FileCacheDownloader(callback, httpClient, url, output, userAgent);
+ }
+
+ private FileCacheDownloader(Callback callback, OkHttpClient httpClient,
+ String url, File output, String userAgent) {
+ this.callback = callback;
+ this.httpClient = httpClient;
+ this.url = url;
+ this.output = output;
+ this.userAgent = userAgent;
+
+ handler = new Handler(Looper.getMainLooper());
+ }
+
+ @MainThread
+ public void execute(ExecutorService executor) {
+ future = executor.submit(this);
+ }
+
+ @MainThread
+ public String getUrl() {
+ return url;
+ }
+
+ @AnyThread
+ public Future> getFuture() {
+ return future;
+ }
+
+ @MainThread
+ public void addListener(FileCacheListener callback) {
+ listeners.add(callback);
+ }
+
+ /**
+ * Cancel this download.
+ */
+ @MainThread
+ public void cancel() {
+ if (cancel.compareAndSet(false, true)) {
+ // Did not start running yet, mark finished here.
+ if (!running.get()) {
+ callback.downloaderFinished(this);
+ }
+ }
+ }
+
+ @AnyThread
+ private void post(Runnable runnable) {
+ handler.post(runnable);
+ }
+
+ @AnyThread
+ private void log(String message) {
+ Logger.d(TAG, logPrefix() + message);
+ }
+
+ @AnyThread
+ private void log(String message, Exception e) {
+ Logger.e(TAG, logPrefix() + message, e);
+ }
+
+ private String logPrefix() {
+ return "[" + url.substring(0, Math.min(url.length(), 45)) + "] ";
+ }
+
+ @Override
+ @WorkerThread
+ public void run() {
+ log("start");
+ running.set(true);
+ execute();
+ }
+
+ @WorkerThread
+ private void execute() {
+ Closeable sourceCloseable = null;
+ Closeable sinkCloseable = null;
+
+ try {
+ checkCancel();
+
+ ResponseBody body = getBody();
+
+ Source source = body.source();
+ sourceCloseable = source;
+
+ BufferedSink sink = Okio.buffer(Okio.sink(output));
+ sinkCloseable = sink;
+
+ checkCancel();
+
+ log("got input stream");
+
+ pipeBody(source, sink);
+
+ log("done");
+
+ post(() -> {
+ callback.downloaderAddedFile(output);
+ callback.downloaderFinished(this);
+ for (FileCacheListener callback : listeners) {
+ callback.onSuccess(output);
+ callback.onEnd();
+ }
+ });
+ } catch (IOException e) {
+ boolean isNotFound = false;
+ boolean cancelled = false;
+ if (e instanceof HttpCodeIOException) {
+ int code = ((HttpCodeIOException) e).code;
+ log("exception: http error, code: " + code, e);
+ isNotFound = code == 404;
+ } else if (e instanceof CancelException) {
+ // Don't log the stack.
+ log("exception: cancelled");
+ cancelled = true;
+ } else {
+ log("exception", e);
+ }
+
+ final boolean finalIsNotFound = isNotFound;
+ final boolean finalCancelled = cancelled;
+ post(() -> {
+ purgeOutput();
+ for (FileCacheListener callback : listeners) {
+ if (finalCancelled) {
+ callback.onCancel();
+ } else {
+ callback.onFail(finalIsNotFound);
+ }
+
+ callback.onEnd();
+ }
+ callback.downloaderFinished(this);
+ });
+ } finally {
+ Util.closeQuietly(sourceCloseable);
+ Util.closeQuietly(sinkCloseable);
+
+ if (call != null) {
+ call.cancel();
+ }
+
+ if (body != null) {
+ Util.closeQuietly(body);
+ }
+ }
+ }
+
+ @WorkerThread
+ private ResponseBody getBody() throws IOException {
+ Request request = new Request.Builder()
+ .url(url)
+ .header("User-Agent", userAgent)
+ .build();
+
+ call = httpClient.newBuilder()
+ .proxy(ChanSettings.getProxy())
+ .build()
+ .newCall(request);
+
+ Response response = call.execute();
+ if (!response.isSuccessful()) {
+ throw new HttpCodeIOException(response.code());
+ }
+
+ checkCancel();
+
+ body = response.body();
+ if (body == null) {
+ throw new IOException("body == null");
+ }
+
+ checkCancel();
+
+ return body;
+ }
+
+ @WorkerThread
+ private void pipeBody(Source source, BufferedSink sink) throws IOException {
+ long contentLength = body.contentLength();
+
+ long read;
+ long total = 0;
+ long notifyTotal = 0;
+
+ Buffer buffer = new Buffer();
+
+ while ((read = source.read(buffer, BUFFER_SIZE)) != -1) {
+ sink.write(buffer, read);
+ total += read;
+
+ if (total >= notifyTotal + NOTIFY_SIZE) {
+ notifyTotal = total;
+ log("progress " + (total / (float) contentLength));
+ postProgress(total, contentLength <= 0 ? total : contentLength);
+ }
+
+ checkCancel();
+ }
+
+ Util.closeQuietly(source);
+ Util.closeQuietly(sink);
+
+ call = null;
+ Util.closeQuietly(body);
+ body = null;
+ }
+
+ @WorkerThread
+ private void checkCancel() throws IOException {
+ if (cancel.get()) {
+ throw new CancelException();
+ }
+ }
+
+ @WorkerThread
+ private void purgeOutput() {
+ if (output.exists()) {
+ final boolean deleteResult = output.delete();
+
+ if (!deleteResult) {
+ log("could not delete the file in purgeOutput");
+ }
+ }
+ }
+
+ @WorkerThread
+ private void postProgress(final long downloaded, final long total) {
+ post(() -> {
+ for (FileCacheListener callback : listeners) {
+ callback.onProgress(downloaded, total);
+ }
+ });
+ }
+
+ private static class CancelException extends IOException {
+ public CancelException() {
+ }
+ }
+
+ private static class HttpCodeIOException extends IOException {
+ private int code;
+
+ public HttpCodeIOException(int code) {
+ this.code = code;
+ }
+ }
+
+ public interface Callback {
+ void downloaderFinished(FileCacheDownloader fileCacheDownloader);
+
+ void downloaderAddedFile(File file);
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/cache/FileCacheListener.java b/Clover/app/src/main/java/org/floens/chan/core/cache/FileCacheListener.java
new file mode 100644
index 00000000..f06f5fb1
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/core/cache/FileCacheListener.java
@@ -0,0 +1,53 @@
+/*
+ * 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.cache;
+
+import java.io.File;
+
+public abstract class FileCacheListener {
+ public void onProgress(long downloaded, long total) {
+ }
+
+ /**
+ * Called when the file download was completed.
+ */
+ public void onSuccess(File file) {
+ }
+
+ /**
+ * Called when there was an error downloading the file.
+ * This is not called when the download was cancelled.
+ *
+ * @param notFound when it was a http 404 error.
+ */
+ public void onFail(boolean notFound) {
+ }
+
+ /**
+ * Called when the file download was cancelled.
+ */
+ public void onCancel() {
+ }
+
+ /**
+ * When the download was ended, this is always called, when it failed, succeeded or was
+ * cancelled.
+ */
+ public void onEnd() {
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/presenter/ImageViewerPresenter.java b/Clover/app/src/main/java/org/floens/chan/core/presenter/ImageViewerPresenter.java
index 4d03b60d..3bcf8d64 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/presenter/ImageViewerPresenter.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/presenter/ImageViewerPresenter.java
@@ -21,12 +21,13 @@ import android.net.ConnectivityManager;
import android.support.v4.view.ViewPager;
import org.floens.chan.core.cache.FileCache;
+import org.floens.chan.core.cache.FileCacheDownloader;
+import org.floens.chan.core.cache.FileCacheListener;
import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.view.MultiImageView;
-import java.io.File;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
@@ -52,7 +53,7 @@ public class ImageViewerPresenter implements MultiImageView.Callback, ViewPager.
private int selectedPosition;
private Loadable loadable;
- private Set preloadingImages = new HashSet<>();
+ private Set preloadingImages = new HashSet<>();
// Disables swiping until the view pager is visible
private boolean viewPagerVisible = false;
@@ -246,28 +247,18 @@ public class ImageViewerPresenter implements MultiImageView.Callback, ViewPager.
// If downloading, remove from preloadingImages if it finished.
// Array to allow access from within the callback (the callback should really
// pass the filecachedownloader itself).
- final FileCache.FileCacheDownloader[] preloadDownload =
- new FileCache.FileCacheDownloader[1];
+ final FileCacheDownloader[] preloadDownload =
+ new FileCacheDownloader[1];
preloadDownload[0] = fileCache.downloadFile(fileUrl,
- new FileCache.DownloadedCallback() {
+ new FileCacheListener() {
@Override
- public void onProgress(long downloaded, long total, boolean done) {
- }
-
- @Override
- public void onSuccess(File file) {
- if (preloadDownload[0] != null) {
- preloadingImages.remove(preloadDownload[0]);
- }
- }
-
- @Override
- public void onFail(boolean notFound) {
+ public void onEnd() {
if (preloadDownload[0] != null) {
preloadingImages.remove(preloadDownload[0]);
}
}
- });
+ }
+ );
if (preloadDownload[0] != null) {
preloadingImages.add(preloadDownload[0]);
@@ -277,7 +268,7 @@ public class ImageViewerPresenter implements MultiImageView.Callback, ViewPager.
}
private void cancelPreloadingImages() {
- for (FileCache.FileCacheDownloader preloadingImage : preloadingImages) {
+ for (FileCacheDownloader preloadingImage : preloadingImages) {
preloadingImage.cancel();
}
preloadingImages.clear();
diff --git a/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java b/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java
index 30a52de5..74d4815a 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java
@@ -22,7 +22,9 @@ import android.graphics.Bitmap;
import android.media.MediaScannerConnection;
import android.net.Uri;
+import org.floens.chan.core.cache.FileCacheListener;
import org.floens.chan.core.cache.FileCache;
+import org.floens.chan.core.cache.FileCacheDownloader;
import org.floens.chan.core.model.PostImage;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils;
@@ -38,7 +40,7 @@ import static org.floens.chan.Chan.inject;
import static org.floens.chan.utils.AndroidUtils.dp;
import static org.floens.chan.utils.AndroidUtils.getAppContext;
-public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback {
+public class ImageSaveTask extends FileCacheListener implements Runnable {
private static final String TAG = "ImageSaveTask";
@Inject
@@ -116,7 +118,8 @@ public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback {
// Manually call postFinished()
postFinished(success);
} else {
- FileCache.FileCacheDownloader fileCacheDownloader = fileCache.downloadFile(postImage.imageUrl.toString(), this);
+ FileCacheDownloader fileCacheDownloader =
+ fileCache.downloadFile(postImage.imageUrl.toString(), this);
// If the fileCacheDownloader is null then the destination already existed and onSuccess() has been called.
// Wait otherwise for the download to finish to avoid that the next task is immediately executed.
@@ -132,15 +135,6 @@ public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback {
}
}
- @Override
- public void onProgress(long downloaded, long total, boolean done) {
- }
-
- @Override
- public void onFail(boolean notFound) {
- postFinished(success);
- }
-
@Override
public void onSuccess(File file) {
if (copyToDestination(file)) {
@@ -148,6 +142,10 @@ public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback {
} else {
deleteDestination();
}
+ }
+
+ @Override
+ public void onEnd() {
postFinished(success);
}
@@ -221,12 +219,8 @@ public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback {
}
private void postFinished(final boolean success) {
- AndroidUtils.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- callback.imageSaveTaskFinished(ImageSaveTask.this, success);
- }
- });
+ AndroidUtils.runOnUiThread(() ->
+ callback.imageSaveTaskFinished(ImageSaveTask.this, success));
}
public interface ImageSaveTaskCallback {
diff --git a/Clover/app/src/main/java/org/floens/chan/core/update/UpdateManager.java b/Clover/app/src/main/java/org/floens/chan/core/update/UpdateManager.java
index 7e2d4303..6d2fcba3 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/update/UpdateManager.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/update/UpdateManager.java
@@ -27,6 +27,7 @@ import android.text.TextUtils;
import com.android.volley.RequestQueue;
import org.floens.chan.BuildConfig;
+import org.floens.chan.core.cache.FileCacheListener;
import org.floens.chan.core.cache.FileCache;
import org.floens.chan.core.net.UpdateApiRequest;
import org.floens.chan.core.settings.ChanSettings;
@@ -164,10 +165,10 @@ public class UpdateManager {
* @param update update with apk details.
*/
public void doUpdate(Update update) {
- fileCache.downloadFile(update.apkUrl.toString(), new FileCache.DownloadedCallback() {
+ fileCache.downloadFile(update.apkUrl.toString(), new FileCacheListener() {
@Override
- public void onProgress(long downloaded, long total, boolean done) {
- if (!done) callback.onUpdateDownloadProgress(downloaded, total);
+ public void onProgress(long downloaded, long total) {
+ callback.onUpdateDownloadProgress(downloaded, total);
}
@Override
@@ -180,6 +181,11 @@ public class UpdateManager {
public void onFail(boolean notFound) {
callback.onUpdateDownloadFailed();
}
+
+ @Override
+ public void onCancel() {
+ callback.onUpdateDownloadFailed();
+ }
});
}
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 c54b444c..aced848f 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
@@ -40,7 +40,9 @@ import com.android.volley.toolbox.ImageLoader.ImageContainer;
import com.davemorrissey.labs.subscaleview.ImageSource;
import org.floens.chan.R;
+import org.floens.chan.core.cache.FileCacheListener;
import org.floens.chan.core.cache.FileCache;
+import org.floens.chan.core.cache.FileCacheDownloader;
import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.utils.AndroidUtils;
@@ -78,9 +80,9 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
private boolean hasContent = false;
private ImageContainer thumbnailRequest;
- private FileCache.FileCacheDownloader bigImageRequest;
- private FileCache.FileCacheDownloader gifRequest;
- private FileCache.FileCacheDownloader videoRequest;
+ private FileCacheDownloader bigImageRequest;
+ private FileCacheDownloader gifRequest;
+ private FileCacheDownloader videoRequest;
private VideoView videoView;
private boolean videoError = false;
@@ -231,30 +233,35 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
}
callback.showProgress(this, true);
- bigImageRequest = fileCache.downloadFile(imageUrl, new FileCache.DownloadedCallback() {
+ bigImageRequest = fileCache.downloadFile(imageUrl, new FileCacheListener() {
@Override
- public void onProgress(long downloaded, long total, boolean done) {
+ public void onProgress(long downloaded, long total) {
callback.onProgress(MultiImageView.this, downloaded, total);
- if (done) {
- callback.showProgress(MultiImageView.this, false);
- }
}
@Override
public void onSuccess(File file) {
- bigImageRequest = null;
setBigImageFile(file);
}
@Override
public void onFail(boolean notFound) {
- bigImageRequest = null;
if (notFound) {
onNotFoundError();
} else {
onError();
}
}
+
+ @Override
+ public void onCancel() {
+ }
+
+ @Override
+ public void onEnd() {
+ bigImageRequest = null;
+ callback.showProgress(MultiImageView.this, false);
+ }
});
}
@@ -273,18 +280,14 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
}
callback.showProgress(this, true);
- gifRequest = fileCache.downloadFile(gifUrl, new FileCache.DownloadedCallback() {
+ gifRequest = fileCache.downloadFile(gifUrl, new FileCacheListener() {
@Override
- public void onProgress(long downloaded, long total, boolean done) {
+ public void onProgress(long downloaded, long total) {
callback.onProgress(MultiImageView.this, downloaded, total);
- if (done) {
- callback.showProgress(MultiImageView.this, false);
- }
}
@Override
public void onSuccess(File file) {
- gifRequest = null;
if (!hasContent || mode == Mode.GIF) {
setGifFile(file);
}
@@ -292,13 +295,22 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
@Override
public void onFail(boolean notFound) {
- gifRequest = null;
if (notFound) {
onNotFoundError();
} else {
onError();
}
}
+
+ @Override
+ public void onCancel() {
+ }
+
+ @Override
+ public void onEnd() {
+ gifRequest = null;
+ callback.showProgress(MultiImageView.this, false);
+ }
});
}
@@ -337,18 +349,14 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
}
callback.showProgress(this, true);
- videoRequest = fileCache.downloadFile(videoUrl, new FileCache.DownloadedCallback() {
+ videoRequest = fileCache.downloadFile(videoUrl, new FileCacheListener() {
@Override
- public void onProgress(long downloaded, long total, boolean done) {
+ public void onProgress(long downloaded, long total) {
callback.onProgress(MultiImageView.this, downloaded, total);
- if (done) {
- callback.showProgress(MultiImageView.this, false);
- }
}
@Override
public void onSuccess(File file) {
- videoRequest = null;
if (!hasContent || mode == Mode.MOVIE) {
setVideoFile(file);
}
@@ -356,13 +364,22 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
@Override
public void onFail(boolean notFound) {
- videoRequest = null;
if (notFound) {
onNotFoundError();
} else {
onError();
}
}
+
+ @Override
+ public void onCancel() {
+ }
+
+ @Override
+ public void onEnd() {
+ videoRequest = null;
+ callback.showProgress(MultiImageView.this, false);
+ }
});
}
diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
index dad104d8..b408942d 100644
--- a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
+++ b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
@@ -352,12 +352,9 @@ public class AndroidUtils {
if (returnIfNotZero && width > 0 && height > 0) {
callback.onMeasured(view);
} else {
- Logger.d(TAG, "Adding OnPreDrawListener to ViewTreeObserver");
viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
- Logger.d(TAG, "OnPreDraw callback");
-
ViewTreeObserver usingViewTreeObserver = viewTreeObserver;
if (viewTreeObserver != view.getViewTreeObserver()) {
Logger.e(TAG, "view.getViewTreeObserver() is another viewtreeobserver! replacing with the new one");
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 9a338839..76ed0c8e 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
@@ -35,7 +35,7 @@ import java.io.StringWriter;
import java.io.Writer;
public class IOUtils {
- private static final int DEFAULT_BUFFER_SIZE = 4096;
+ private static final int DEFAULT_BUFFER_SIZE = 8192;
public static String assetAsString(Context context, String assetName) {
String res = null;