Refactor FileCache

refactor-toolbar
Floens 7 years ago
parent 83ae7a297f
commit 8a13d9fb2d
  1. 175
      Clover/app/src/main/java/org/floens/chan/core/cache/CacheHandler.java
  2. 448
      Clover/app/src/main/java/org/floens/chan/core/cache/FileCache.java
  3. 331
      Clover/app/src/main/java/org/floens/chan/core/cache/FileCacheDownloader.java
  4. 53
      Clover/app/src/main/java/org/floens/chan/core/cache/FileCacheListener.java
  5. 27
      Clover/app/src/main/java/org/floens/chan/core/presenter/ImageViewerPresenter.java
  6. 28
      Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java
  7. 12
      Clover/app/src/main/java/org/floens/chan/core/update/UpdateManager.java
  8. 65
      Clover/app/src/main/java/org/floens/chan/ui/view/MultiImageView.java
  9. 3
      Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
  10. 2
      Clover/app/src/main/java/org/floens/chan/utils/IOUtils.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 <http://www.gnu.org/licenses/>.
*/
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<Pair<File, Long>> 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());
}
}

@ -17,57 +17,36 @@
*/ */
package org.floens.chan.core.cache; package org.floens.chan.core.cache;
import org.floens.chan.core.settings.ChanSettings; import android.support.annotation.MainThread;
import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.Logger; import org.floens.chan.utils.Logger;
import org.floens.chan.utils.Time; import org.floens.chan.utils.Time;
import java.io.BufferedOutputStream;
import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit; 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.OkHttpClient;
import okhttp3.Protocol; 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 String TAG = "FileCache";
private static final int TIMEOUT = 10000; private static final int TIMEOUT = 10000;
private static final int TRIM_TRIES = 20; private static final int DOWNLOAD_POOL_SIZE = 2;
private static final int THREAD_COUNT = 2;
private static final ExecutorService executor = Executors.newFixedThreadPool(THREAD_COUNT); private final ExecutorService downloadPool = Executors.newFixedThreadPool(DOWNLOAD_POOL_SIZE);
private String userAgent; private String userAgent;
private OkHttpClient httpClient; protected OkHttpClient httpClient;
private final File directory; private final CacheHandler cacheHandler;
private final long maxSize;
private AtomicLong size = new AtomicLong();
private AtomicBoolean trimRunning = new AtomicBoolean(false);
private List<FileCacheDownloader> downloaders = new ArrayList<>(); private List<FileCacheDownloader> downloaders = new ArrayList<>();
public FileCache(File directory, long maxSize, String userAgent) { public FileCache(File directory, long maxSize, String userAgent) {
this.directory = directory;
this.maxSize = maxSize;
this.userAgent = userAgent; this.userAgent = userAgent;
httpClient = new OkHttpClient.Builder() httpClient = new OkHttpClient.Builder()
@ -78,411 +57,88 @@ public class FileCache {
.protocols(Collections.singletonList(Protocol.HTTP_1_1)) .protocols(Collections.singletonList(Protocol.HTTP_1_1))
.build(); .build();
createDirectories(); cacheHandler = new CacheHandler(directory, maxSize);
recalculateSize();
} }
public void clearCache() { public void clearCache() {
Logger.d(TAG, "Clearing cache");
for (FileCacheDownloader downloader : downloaders) { for (FileCacheDownloader downloader : downloaders) {
downloader.cancel(); downloader.cancel();
} }
if (directory.exists() && directory.isDirectory()) { cacheHandler.clearCache();
for (File file : directory.listFiles()) {
if (!file.delete()) {
Logger.d(TAG, "Could not delete cache file while clearing cache " + file.getName());
}
}
}
recalculateSize();
} }
/** /**
* Start downloading the file located at the url.<br> * Start downloading the file located at the url.<br>
* If the file is in the cache then the callback is executed immediately and null is returned.<br> * If the file is in the cache then the callback is executed immediately and null is
* Otherwise if the file is downloading or has not yet started downloading an {@link FileCacheDownloader} is returned.<br> * returned.<br>
* Only call this method on the UI thread.<br> * Otherwise if the file is downloading or has not yet started downloading a
* {@link FileCacheDownloader} is returned.<br>
* *
* @param urlString the url to download. * @param url the url to download.
* @param callback callback to execute callbacks on. * @param listener listener to execute callbacks on.
* @return null if in the cache, {@link FileCacheDownloader} otherwise. * @return {@code null} if in the cache, {@link FileCacheDownloader} otherwise.
*/ */
public FileCacheDownloader downloadFile(final String urlString, final DownloadedCallback callback) { @MainThread
FileCacheDownloader downloader = null; public FileCacheDownloader downloadFile(String url, FileCacheListener listener) {
for (FileCacheDownloader downloaderItem : downloaders) { FileCacheDownloader runningDownloaderForKey = getDownloaderByKey(url);
if (downloaderItem.getUrl().equals(urlString)) { if (runningDownloaderForKey != null) {
downloader = downloaderItem; runningDownloaderForKey.addListener(listener);
break; return runningDownloaderForKey;
}
} }
if (downloader != null) { File file = get(url);
downloader.addCallback(callback);
return downloader;
} else {
File file = get(urlString);
if (file.exists()) { if (file.exists()) {
// TODO: setLastModified doesn't seem to work on Android... handleFileImmediatelyAvailable(listener, file);
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; return null;
} else { } else {
FileCacheDownloader newDownloader = new FileCacheDownloader(this, urlString, file, userAgent); return handleStartDownload(listener, file, url);
newDownloader.addCallback(callback);
Future<?> future = executor.submit(newDownloader);
newDownloader.setFuture(future);
downloaders.add(newDownloader);
return newDownloader;
}
}
}
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();
}
}
}
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<File> 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();
}
}
// 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);
}
private void removeFromDownloaders(FileCacheDownloader downloader) {
downloaders.remove(downloader);
}
public interface DownloadedCallback {
void onProgress(long downloaded, long total, boolean done);
void onSuccess(File file);
void onFail(boolean notFound);
}
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<DownloadedCallback> 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;
}
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); public FileCacheDownloader getDownloaderByKey(String key) {
for (FileCacheDownloader downloader : downloaders) {
post(new Runnable() { if (downloader.getUrl().equals(key)) {
@Override return downloader;
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();
} }
}); return null;
} }
private void success() {
Logger.d(TAG, "Success downloading " + url);
post(new Runnable() {
@Override @Override
public void run() { public void downloaderFinished(FileCacheDownloader fileCacheDownloader) {
fileCache.fileWasAdded(output); downloaders.remove(fileCacheDownloader);
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 @Override
public void run() { public void downloaderAddedFile(File file) {
for (DownloadedCallback callback : callbacks) { cacheHandler.fileWasAdded(file);
callback.onProgress(downloaded, total, done);
}
}
});
} }
private void post(Runnable runnable) { public boolean exists(String key) {
AndroidUtils.runOnUiThread(runnable); return cacheHandler.exists(key);
} }
private void execute() throws Exception { public File get(String key) {
Request request = new Request.Builder() return cacheHandler.get(key);
.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(); private void handleFileImmediatelyAvailable(FileCacheListener listener, File file) {
long contentLength = body.contentLength(); // TODO: setLastModified doesn't seem to work on Android...
BufferedSource source = body.source(); if (!file.setLastModified(Time.get())) {
OutputStream outputStream = new BufferedOutputStream(new FileOutputStream(output)); Logger.e(TAG, "Could not set last modified time on file");
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);
} }
listener.onSuccess(file);
listener.onEnd();
} }
if (Thread.currentThread().isInterrupted()) throw new InterruptedIOException(); private FileCacheDownloader handleStartDownload(
FileCacheListener listener, File file, String url) {
success(); FileCacheDownloader downloader = FileCacheDownloader.fromCallbackClientUrlOutputUserAgent(
} this, httpClient, url, file, userAgent);
downloader.addListener(listener);
downloader.execute(downloadPool);
downloaders.add(downloader);
return downloader;
} }
} }

@ -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 <http://www.gnu.org/licenses/>.
*/
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<FileCacheListener> 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);
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
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.
* <b>This is not called when the download was cancelled.</b>
*
* @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() {
}
}

@ -21,12 +21,13 @@ import android.net.ConnectivityManager;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import org.floens.chan.core.cache.FileCache; 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.PostImage;
import org.floens.chan.core.model.orm.Loadable; import org.floens.chan.core.model.orm.Loadable;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.view.MultiImageView; import org.floens.chan.ui.view.MultiImageView;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
@ -52,7 +53,7 @@ public class ImageViewerPresenter implements MultiImageView.Callback, ViewPager.
private int selectedPosition; private int selectedPosition;
private Loadable loadable; private Loadable loadable;
private Set<FileCache.FileCacheDownloader> preloadingImages = new HashSet<>(); private Set<FileCacheDownloader> preloadingImages = new HashSet<>();
// Disables swiping until the view pager is visible // Disables swiping until the view pager is visible
private boolean viewPagerVisible = false; private boolean viewPagerVisible = false;
@ -246,28 +247,18 @@ public class ImageViewerPresenter implements MultiImageView.Callback, ViewPager.
// If downloading, remove from preloadingImages if it finished. // If downloading, remove from preloadingImages if it finished.
// Array to allow access from within the callback (the callback should really // Array to allow access from within the callback (the callback should really
// pass the filecachedownloader itself). // pass the filecachedownloader itself).
final FileCache.FileCacheDownloader[] preloadDownload = final FileCacheDownloader[] preloadDownload =
new FileCache.FileCacheDownloader[1]; new FileCacheDownloader[1];
preloadDownload[0] = fileCache.downloadFile(fileUrl, preloadDownload[0] = fileCache.downloadFile(fileUrl,
new FileCache.DownloadedCallback() { new FileCacheListener() {
@Override @Override
public void onProgress(long downloaded, long total, boolean done) { public void onEnd() {
}
@Override
public void onSuccess(File file) {
if (preloadDownload[0] != null) { if (preloadDownload[0] != null) {
preloadingImages.remove(preloadDownload[0]); preloadingImages.remove(preloadDownload[0]);
} }
} }
@Override
public void onFail(boolean notFound) {
if (preloadDownload[0] != null) {
preloadingImages.remove(preloadDownload[0]);
}
} }
}); );
if (preloadDownload[0] != null) { if (preloadDownload[0] != null) {
preloadingImages.add(preloadDownload[0]); preloadingImages.add(preloadDownload[0]);
@ -277,7 +268,7 @@ public class ImageViewerPresenter implements MultiImageView.Callback, ViewPager.
} }
private void cancelPreloadingImages() { private void cancelPreloadingImages() {
for (FileCache.FileCacheDownloader preloadingImage : preloadingImages) { for (FileCacheDownloader preloadingImage : preloadingImages) {
preloadingImage.cancel(); preloadingImage.cancel();
} }
preloadingImages.clear(); preloadingImages.clear();

@ -22,7 +22,9 @@ import android.graphics.Bitmap;
import android.media.MediaScannerConnection; import android.media.MediaScannerConnection;
import android.net.Uri; import android.net.Uri;
import org.floens.chan.core.cache.FileCacheListener;
import org.floens.chan.core.cache.FileCache; 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.model.PostImage;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
import org.floens.chan.utils.IOUtils; 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.dp;
import static org.floens.chan.utils.AndroidUtils.getAppContext; 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"; private static final String TAG = "ImageSaveTask";
@Inject @Inject
@ -116,7 +118,8 @@ public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback {
// Manually call postFinished() // Manually call postFinished()
postFinished(success); postFinished(success);
} else { } 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. // 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. // 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 @Override
public void onSuccess(File file) { public void onSuccess(File file) {
if (copyToDestination(file)) { if (copyToDestination(file)) {
@ -148,6 +142,10 @@ public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback {
} else { } else {
deleteDestination(); deleteDestination();
} }
}
@Override
public void onEnd() {
postFinished(success); postFinished(success);
} }
@ -221,12 +219,8 @@ public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback {
} }
private void postFinished(final boolean success) { private void postFinished(final boolean success) {
AndroidUtils.runOnUiThread(new Runnable() { AndroidUtils.runOnUiThread(() ->
@Override callback.imageSaveTaskFinished(ImageSaveTask.this, success));
public void run() {
callback.imageSaveTaskFinished(ImageSaveTask.this, success);
}
});
} }
public interface ImageSaveTaskCallback { public interface ImageSaveTaskCallback {

@ -27,6 +27,7 @@ import android.text.TextUtils;
import com.android.volley.RequestQueue; import com.android.volley.RequestQueue;
import org.floens.chan.BuildConfig; import org.floens.chan.BuildConfig;
import org.floens.chan.core.cache.FileCacheListener;
import org.floens.chan.core.cache.FileCache; import org.floens.chan.core.cache.FileCache;
import org.floens.chan.core.net.UpdateApiRequest; import org.floens.chan.core.net.UpdateApiRequest;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
@ -164,10 +165,10 @@ public class UpdateManager {
* @param update update with apk details. * @param update update with apk details.
*/ */
public void doUpdate(Update update) { public void doUpdate(Update update) {
fileCache.downloadFile(update.apkUrl.toString(), new FileCache.DownloadedCallback() { fileCache.downloadFile(update.apkUrl.toString(), new FileCacheListener() {
@Override @Override
public void onProgress(long downloaded, long total, boolean done) { public void onProgress(long downloaded, long total) {
if (!done) callback.onUpdateDownloadProgress(downloaded, total); callback.onUpdateDownloadProgress(downloaded, total);
} }
@Override @Override
@ -180,6 +181,11 @@ public class UpdateManager {
public void onFail(boolean notFound) { public void onFail(boolean notFound) {
callback.onUpdateDownloadFailed(); callback.onUpdateDownloadFailed();
} }
@Override
public void onCancel() {
callback.onUpdateDownloadFailed();
}
}); });
} }

@ -40,7 +40,9 @@ import com.android.volley.toolbox.ImageLoader.ImageContainer;
import com.davemorrissey.labs.subscaleview.ImageSource; import com.davemorrissey.labs.subscaleview.ImageSource;
import org.floens.chan.R; 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.FileCache;
import org.floens.chan.core.cache.FileCacheDownloader;
import org.floens.chan.core.model.PostImage; import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.settings.ChanSettings; import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.utils.AndroidUtils; import org.floens.chan.utils.AndroidUtils;
@ -78,9 +80,9 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
private boolean hasContent = false; private boolean hasContent = false;
private ImageContainer thumbnailRequest; private ImageContainer thumbnailRequest;
private FileCache.FileCacheDownloader bigImageRequest; private FileCacheDownloader bigImageRequest;
private FileCache.FileCacheDownloader gifRequest; private FileCacheDownloader gifRequest;
private FileCache.FileCacheDownloader videoRequest; private FileCacheDownloader videoRequest;
private VideoView videoView; private VideoView videoView;
private boolean videoError = false; private boolean videoError = false;
@ -231,30 +233,35 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
} }
callback.showProgress(this, true); callback.showProgress(this, true);
bigImageRequest = fileCache.downloadFile(imageUrl, new FileCache.DownloadedCallback() { bigImageRequest = fileCache.downloadFile(imageUrl, new FileCacheListener() {
@Override @Override
public void onProgress(long downloaded, long total, boolean done) { public void onProgress(long downloaded, long total) {
callback.onProgress(MultiImageView.this, downloaded, total); callback.onProgress(MultiImageView.this, downloaded, total);
if (done) {
callback.showProgress(MultiImageView.this, false);
}
} }
@Override @Override
public void onSuccess(File file) { public void onSuccess(File file) {
bigImageRequest = null;
setBigImageFile(file); setBigImageFile(file);
} }
@Override @Override
public void onFail(boolean notFound) { public void onFail(boolean notFound) {
bigImageRequest = null;
if (notFound) { if (notFound) {
onNotFoundError(); onNotFoundError();
} else { } else {
onError(); 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); callback.showProgress(this, true);
gifRequest = fileCache.downloadFile(gifUrl, new FileCache.DownloadedCallback() { gifRequest = fileCache.downloadFile(gifUrl, new FileCacheListener() {
@Override @Override
public void onProgress(long downloaded, long total, boolean done) { public void onProgress(long downloaded, long total) {
callback.onProgress(MultiImageView.this, downloaded, total); callback.onProgress(MultiImageView.this, downloaded, total);
if (done) {
callback.showProgress(MultiImageView.this, false);
}
} }
@Override @Override
public void onSuccess(File file) { public void onSuccess(File file) {
gifRequest = null;
if (!hasContent || mode == Mode.GIF) { if (!hasContent || mode == Mode.GIF) {
setGifFile(file); setGifFile(file);
} }
@ -292,13 +295,22 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
@Override @Override
public void onFail(boolean notFound) { public void onFail(boolean notFound) {
gifRequest = null;
if (notFound) { if (notFound) {
onNotFoundError(); onNotFoundError();
} else { } else {
onError(); 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); callback.showProgress(this, true);
videoRequest = fileCache.downloadFile(videoUrl, new FileCache.DownloadedCallback() { videoRequest = fileCache.downloadFile(videoUrl, new FileCacheListener() {
@Override @Override
public void onProgress(long downloaded, long total, boolean done) { public void onProgress(long downloaded, long total) {
callback.onProgress(MultiImageView.this, downloaded, total); callback.onProgress(MultiImageView.this, downloaded, total);
if (done) {
callback.showProgress(MultiImageView.this, false);
}
} }
@Override @Override
public void onSuccess(File file) { public void onSuccess(File file) {
videoRequest = null;
if (!hasContent || mode == Mode.MOVIE) { if (!hasContent || mode == Mode.MOVIE) {
setVideoFile(file); setVideoFile(file);
} }
@ -356,13 +364,22 @@ public class MultiImageView extends FrameLayout implements View.OnClickListener
@Override @Override
public void onFail(boolean notFound) { public void onFail(boolean notFound) {
videoRequest = null;
if (notFound) { if (notFound) {
onNotFoundError(); onNotFoundError();
} else { } else {
onError(); onError();
} }
} }
@Override
public void onCancel() {
}
@Override
public void onEnd() {
videoRequest = null;
callback.showProgress(MultiImageView.this, false);
}
}); });
} }

@ -352,12 +352,9 @@ public class AndroidUtils {
if (returnIfNotZero && width > 0 && height > 0) { if (returnIfNotZero && width > 0 && height > 0) {
callback.onMeasured(view); callback.onMeasured(view);
} else { } else {
Logger.d(TAG, "Adding OnPreDrawListener to ViewTreeObserver");
viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override @Override
public boolean onPreDraw() { public boolean onPreDraw() {
Logger.d(TAG, "OnPreDraw callback");
ViewTreeObserver usingViewTreeObserver = viewTreeObserver; ViewTreeObserver usingViewTreeObserver = viewTreeObserver;
if (viewTreeObserver != view.getViewTreeObserver()) { if (viewTreeObserver != view.getViewTreeObserver()) {
Logger.e(TAG, "view.getViewTreeObserver() is another viewtreeobserver! replacing with the new one"); Logger.e(TAG, "view.getViewTreeObserver() is another viewtreeobserver! replacing with the new one");

@ -35,7 +35,7 @@ import java.io.StringWriter;
import java.io.Writer; import java.io.Writer;
public class IOUtils { 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) { public static String assetAsString(Context context, String assetName) {
String res = null; String res = null;

Loading…
Cancel
Save