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