From 8a13d9fb2d7372aff3e74ddc41375995e286f77f Mon Sep 17 00:00:00 2001 From: Floens Date: Wed, 28 Mar 2018 12:20:13 +0200 Subject: [PATCH] Refactor FileCache --- .../floens/chan/core/cache/CacheHandler.java | 175 +++++++ .../org/floens/chan/core/cache/FileCache.java | 462 +++--------------- .../chan/core/cache/FileCacheDownloader.java | 331 +++++++++++++ .../chan/core/cache/FileCacheListener.java | 53 ++ .../core/presenter/ImageViewerPresenter.java | 29 +- .../floens/chan/core/saver/ImageSaveTask.java | 28 +- .../chan/core/update/UpdateManager.java | 12 +- .../floens/chan/ui/view/MultiImageView.java | 65 ++- .../org/floens/chan/utils/AndroidUtils.java | 3 - .../java/org/floens/chan/utils/IOUtils.java | 2 +- 10 files changed, 690 insertions(+), 470 deletions(-) create mode 100644 Clover/app/src/main/java/org/floens/chan/core/cache/CacheHandler.java create mode 100644 Clover/app/src/main/java/org/floens/chan/core/cache/FileCacheDownloader.java create mode 100644 Clover/app/src/main/java/org/floens/chan/core/cache/FileCacheListener.java 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;