mirror of https://github.com/kurisufriend/Clover
parent
83ae7a297f
commit
8a13d9fb2d
@ -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()); |
||||
} |
||||
} |
@ -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() { |
||||
} |
||||
} |
Loading…
Reference in new issue