From d4dc65a4bac3c5ac9128a56bc3cdd9c340c2bc7c Mon Sep 17 00:00:00 2001 From: tarsin <43900799+asuka-mio@users.noreply.github.com> Date: Mon, 16 Jan 2023 21:44:25 +0800 Subject: [PATCH] Drop old disklrucache (#685) * Drop Old DiskLruCache * Avoid being interrupted when completing editor * Protect all journal operations * Revert "Protect all journal operations" This reverts commit 5b7c7e3b33a515ee3e92826a6b76c2dfb3040c5d. * Revert "Avoid being interrupted when completing editor" This reverts commit cf4fbed1281eff766c6e65b6f685dfc355208a0b. * Shutdown Thread gracefully Test: m, Check no thread leaks * Fix download extensions --- .../com/hippo/beerbelly/DiskLruCache.java | 985 ------------------ .../com/hippo/beerbelly/SimpleDiskCache.java | 504 --------- .../com/hippo/beerbelly/StrictLineReader.java | 193 ---- .../main/java/com/hippo/beerbelly/Util.java | 102 -- .../com/hippo/ehviewer/spider/SpiderDen.java | 104 +- .../hippo/ehviewer/spider/SpiderQueen.java | 82 +- 6 files changed, 114 insertions(+), 1856 deletions(-) delete mode 100644 app/src/main/java/com/hippo/beerbelly/DiskLruCache.java delete mode 100644 app/src/main/java/com/hippo/beerbelly/SimpleDiskCache.java delete mode 100644 app/src/main/java/com/hippo/beerbelly/StrictLineReader.java delete mode 100644 app/src/main/java/com/hippo/beerbelly/Util.java diff --git a/app/src/main/java/com/hippo/beerbelly/DiskLruCache.java b/app/src/main/java/com/hippo/beerbelly/DiskLruCache.java deleted file mode 100644 index c9a332a89..000000000 --- a/app/src/main/java/com/hippo/beerbelly/DiskLruCache.java +++ /dev/null @@ -1,985 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.beerbelly; - -import java.io.BufferedWriter; -import java.io.Closeable; -import java.io.EOFException; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * A cache that uses a bounded amount of space on a filesystem. Each cache - * entry has a string key and a fixed number of values. Each key must match - * the regex [a-z0-9_-]{1,120}. Values are byte sequences, - * accessible as streams or files. Each value must be between {@code 0} and - * {@code Integer.MAX_VALUE} bytes in length. - * - *

The cache stores its data in a directory on the filesystem. This - * directory must be exclusive to the cache; the cache may delete or overwrite - * files from its directory. It is an error for multiple processes to use the - * same cache directory at the same time. - * - *

This cache limits the number of bytes that it will store on the - * filesystem. When the number of stored bytes exceeds the limit, the cache will - * remove entries in the background until the limit is satisfied. The limit is - * not strict: the cache may temporarily exceed it while waiting for files to be - * deleted. The limit does not include filesystem overhead or the cache - * journal so space-sensitive applications should set a conservative limit. - * - *

Clients call {@link #edit} to create or update the values of an entry. An - * entry may have only one editor at one time; if a value is not available to be - * edited then {@link #edit} will return null. - *

- * Every {@link #edit} call must be matched by a call to {@link Editor#commit} - * or {@link Editor#abort}. Committing is atomic: a read observes the full set - * of values as they were before or after the commit, but never a mix of values. - * - *

Clients call {@link #get} to read a snapshot of an entry. The read will - * observe the value at the time that {@link #get} was called. Updates and - * removals after the call do not impact ongoing reads. - * - *

This class is tolerant of some I/O errors. If files are missing from the - * filesystem, the corresponding entries will be dropped from the cache. If - * an error occurs while writing a cache value, the edit will fail silently. - * Callers should handle other problems by catching {@code IOException} and - * responding appropriately. - */ -public final class DiskLruCache implements Closeable { - static final String JOURNAL_FILE = "journal"; - static final String JOURNAL_FILE_TEMP = "journal.tmp"; - static final String JOURNAL_FILE_BACKUP = "journal.bkp"; - static final String MAGIC = "libcore.io.DiskLruCache"; - static final String VERSION_1 = "1"; - static final long ANY_SEQUENCE_NUMBER = -1; - static final String STRING_KEY_PATTERN = "[a-z0-9_-]{1,120}"; - static final Pattern LEGAL_KEY_PATTERN = Pattern.compile(STRING_KEY_PATTERN); - private static final String CLEAN = "CLEAN"; - private static final String DIRTY = "DIRTY"; - private static final String REMOVE = "REMOVE"; - private static final String READ = "READ"; - - /* - * This cache uses a journal file named "journal". A typical journal file - * looks like this: - * libcore.io.DiskLruCache - * 1 - * 100 - * 2 - * - * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 - * DIRTY 335c4c6028171cfddfbaae1a9c313c52 - * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 - * REMOVE 335c4c6028171cfddfbaae1a9c313c52 - * DIRTY 1ab96a171faeeee38496d8b330771a7a - * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 - * READ 335c4c6028171cfddfbaae1a9c313c52 - * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 - * - * The first five lines of the journal form its header. They are the - * constant string "libcore.io.DiskLruCache", the disk cache's version, - * the application's version, the value count, and a blank line. - * - * Each of the subsequent lines in the file is a record of the state of a - * cache entry. Each line contains space-separated values: a state, a key, - * and optional state-specific values. - * o DIRTY lines track that an entry is actively being created or updated. - * Every successful DIRTY action should be followed by a CLEAN or REMOVE - * action. DIRTY lines without a matching CLEAN or REMOVE indicate that - * temporary files may need to be deleted. - * o CLEAN lines track a cache entry that has been successfully published - * and may be read. A publish line is followed by the lengths of each of - * its values. - * o READ lines track accesses for LRU. - * o REMOVE lines track entries that have been deleted. - * - * The journal file is appended to as cache operations occur. The journal may - * occasionally be compacted by dropping redundant lines. A temporary file named - * "journal.tmp" will be used during compaction; that file should be deleted if - * it exists when the cache is opened. - */ - private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { - @Override - public void write(int b) { - // Eat all writes silently. Nom nom. - } - }; - /** - * This cache uses a single background thread to evict entries. - */ - final ThreadPoolExecutor executorService = - new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); - private final File directory; - private final File journalFile; - private final File journalFileTmp; - private final File journalFileBackup; - private final int appVersion; - private final int valueCount; - private final LinkedHashMap lruEntries = - new LinkedHashMap<>(0, 0.75f, true); - private long maxSize; - private long size = 0; - private Writer journalWriter; - private int redundantOpCount; - /** - * To differentiate between old and current snapshots, each entry is given - * a sequence number each time an edit is committed. A snapshot is stale if - * its sequence number is not equal to its entry's sequence number. - */ - private long nextSequenceNumber = 0; - - private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { - this.directory = directory; - this.appVersion = appVersion; - this.journalFile = new File(directory, JOURNAL_FILE); - this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); - this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); - this.valueCount = valueCount; - this.maxSize = maxSize; - } - - /** - * Opens the cache in {@code directory}, creating a cache if none exists - * there. - * - * @param directory a writable directory - * @param valueCount the number of values per cache entry. Must be positive. - * @param maxSize the maximum number of bytes this cache should use to store - * @throws IOException if reading or writing the cache directory fails - */ - public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) - throws IOException { - if (maxSize <= 0) { - throw new IllegalArgumentException("maxSize <= 0"); - } - if (valueCount <= 0) { - throw new IllegalArgumentException("valueCount <= 0"); - } - - // If a bkp file exists, use it instead. - File backupFile = new File(directory, JOURNAL_FILE_BACKUP); - if (backupFile.exists()) { - File journalFile = new File(directory, JOURNAL_FILE); - // If journal file also exists just delete backup file. - if (journalFile.exists()) { - backupFile.delete(); - } else { - renameTo(backupFile, journalFile, false); - } - } - - // Prefer to pick up where we left off. - DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); - if (cache.journalFile.exists()) { - try { - cache.readJournal(); - cache.processJournal(); - return cache; - } catch (IOException journalIsCorrupt) { - System.out - .println("DiskLruCache " - + directory - + " is corrupt: " - + journalIsCorrupt.getMessage() - + ", removing"); - cache.delete(); - } - } - - // Create a new empty cache. - directory.mkdirs(); - cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); - cache.rebuildJournal(); - return cache; - } - - private static void deleteIfExists(File file) throws IOException { - if (file.exists() && !file.delete()) { - throw new IOException(); - } - } - - private static void renameTo(File from, File to, boolean deleteDestination) throws IOException { - if (deleteDestination) { - deleteIfExists(to); - } - if (!from.renameTo(to)) { - throw new IOException(); - } - } - - private static String inputStreamToString(InputStream in) throws IOException { - return Util.readFully(new InputStreamReader(in, Util.UTF_8)); - } - - private void readJournal() throws IOException { - StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII); - try { - String magic = reader.readLine(); - String version = reader.readLine(); - String appVersionString = reader.readLine(); - String valueCountString = reader.readLine(); - String blank = reader.readLine(); - if (!MAGIC.equals(magic) - || !VERSION_1.equals(version) - || !Integer.toString(appVersion).equals(appVersionString) - || !Integer.toString(valueCount).equals(valueCountString) - || !"".equals(blank)) { - throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " - + valueCountString + ", " + blank + "]"); - } - - int lineCount = 0; - while (true) { - try { - readJournalLine(reader.readLine()); - lineCount++; - } catch (EOFException endOfJournal) { - break; - } - } - redundantOpCount = lineCount - lruEntries.size(); - - // If we ended on a truncated line, rebuild the journal before appending to it. - if (reader.hasUnterminatedLine()) { - rebuildJournal(); - } else { - journalWriter = new BufferedWriter(new OutputStreamWriter( - new FileOutputStream(journalFile, true), Util.US_ASCII)); - } - } finally { - Util.closeQuietly(reader); - } - } - - private void readJournalLine(String line) throws IOException { - int firstSpace = line.indexOf(' '); - if (firstSpace == -1) { - throw new IOException("unexpected journal line: " + line); - } - - int keyBegin = firstSpace + 1; - int secondSpace = line.indexOf(' ', keyBegin); - final String key; - if (secondSpace == -1) { - key = line.substring(keyBegin); - if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { - lruEntries.remove(key); - return; - } - } else { - key = line.substring(keyBegin, secondSpace); - } - - Entry entry = lruEntries.get(key); - if (entry == null) { - entry = new Entry(key); - lruEntries.put(key, entry); - } - - if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) { - String[] parts = line.substring(secondSpace + 1).split(" "); - entry.readable = true; - entry.currentEditor = null; - entry.setLengths(parts); - } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { - entry.currentEditor = new Editor(entry); - } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { - // This work was already done by calling lruEntries.get(). - } else { - throw new IOException("unexpected journal line: " + line); - } - } - - /** - * Computes the initial size and collects garbage as a part of opening the - * cache. Dirty entries are assumed to be inconsistent and will be deleted. - */ - private void processJournal() throws IOException { - deleteIfExists(journalFileTmp); - for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) { - Entry entry = i.next(); - if (entry.currentEditor == null) { - for (int t = 0; t < valueCount; t++) { - size += entry.lengths[t]; - } - } else { - entry.currentEditor = null; - for (int t = 0; t < valueCount; t++) { - deleteIfExists(entry.getCleanFile(t)); - deleteIfExists(entry.getDirtyFile(t)); - } - i.remove(); - } - } - } - - /** - * Creates a new journal that omits redundant information. This replaces the - * current journal if it exists. - */ - private synchronized void rebuildJournal() throws IOException { - if (journalWriter != null) { - journalWriter.close(); - } - - try (Writer writer = new BufferedWriter( - new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII))) { - writer.write(MAGIC); - writer.write("\n"); - writer.write(VERSION_1); - writer.write("\n"); - writer.write(Integer.toString(appVersion)); - writer.write("\n"); - writer.write(Integer.toString(valueCount)); - writer.write("\n"); - writer.write("\n"); - - for (Entry entry : lruEntries.values()) { - if (entry.currentEditor != null) { - writer.write(DIRTY + ' ' + entry.key + '\n'); - } else { - writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); - } - } - } - - if (journalFile.exists()) { - renameTo(journalFile, journalFileBackup, true); - } - renameTo(journalFileTmp, journalFile, false); - journalFileBackup.delete(); - - journalWriter = new BufferedWriter( - new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); - } private final Callable cleanupCallable = new Callable<>() { - public Void call() throws Exception { - synchronized (DiskLruCache.this) { - if (journalWriter == null) { - return null; // Closed. - } - trimToSize(); - if (journalRebuildRequired()) { - rebuildJournal(); - redundantOpCount = 0; - } - } - return null; - } - }; - - /** - * Return {@code true} if it contains the key - */ - public synchronized boolean contain(String key) { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - return entry != null && entry.readable; - } - - /** - * Returns a snapshot of the entry named {@code key}, or null if it doesn't - * exist is not currently readable. If a value is returned, it is moved to - * the head of the LRU queue. - */ - public synchronized Snapshot get(String key) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (entry == null) { - return null; - } - - if (!entry.readable) { - return null; - } - - // Open all streams eagerly to guarantee that we see a single published - // snapshot. If we opened streams lazily then the streams could come - // from different edits. - InputStream[] ins = new InputStream[valueCount]; - try { - for (int i = 0; i < valueCount; i++) { - ins[i] = new FileInputStream(entry.getCleanFile(i)); - } - } catch (FileNotFoundException e) { - // A file must have been deleted manually! - for (int i = 0; i < valueCount; i++) { - if (ins[i] != null) { - Util.closeQuietly(ins[i]); - } else { - break; - } - } - return null; - } - - redundantOpCount++; - journalWriter.append(READ + ' ').append(key).append(String.valueOf('\n')); - if (journalRebuildRequired()) { - executorService.submit(cleanupCallable); - } - - return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); - } - - /** - * Returns an editor for the entry named {@code key}, or null if another - * edit is in progress. - */ - public Editor edit(String key) throws IOException { - return edit(key, ANY_SEQUENCE_NUMBER); - } - - private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null - || entry.sequenceNumber != expectedSequenceNumber)) { - return null; // Snapshot is stale. - } - if (entry == null) { - entry = new Entry(key); - lruEntries.put(key, entry); - } else if (entry.currentEditor != null) { - return null; // Another edit is in progress. - } - - Editor editor = new Editor(entry); - entry.currentEditor = editor; - - // Flush the journal before creating files to prevent file leaks. - journalWriter.write(DIRTY + ' ' + key + '\n'); - journalWriter.flush(); - return editor; - } - - /** - * Returns the directory where this cache stores its data. - */ - public File getDirectory() { - return directory; - } - - /** - * Returns the maximum number of bytes that this cache should use to store - * its data. - */ - public synchronized long getMaxSize() { - return maxSize; - } - - /** - * Changes the maximum number of bytes the cache can store and queues a job - * to trim the existing store, if necessary. - */ - public synchronized void setMaxSize(long maxSize) { - this.maxSize = maxSize; - executorService.submit(cleanupCallable); - } - - /** - * Returns the number of bytes currently being used to store the values in - * this cache. This may be greater than the max size if a background - * deletion is pending. - */ - public synchronized long size() { - return size; - } - - private synchronized void completeEdit(Editor editor, boolean success) throws IOException { - Entry entry = editor.entry; - if (entry.currentEditor != editor) { - throw new IllegalStateException(); - } - - // If this edit is creating the entry for the first time, every index must have a value. - if (success && !entry.readable) { - for (int i = 0; i < valueCount; i++) { - if (!editor.written[i]) { - editor.abort(); - throw new IllegalStateException("Newly created entry didn't create value for index " + i); - } - if (!entry.getDirtyFile(i).exists()) { - editor.abort(); - return; - } - } - } - - for (int i = 0; i < valueCount; i++) { - File dirty = entry.getDirtyFile(i); - if (success) { - if (dirty.exists()) { - File clean = entry.getCleanFile(i); - dirty.renameTo(clean); - long oldLength = entry.lengths[i]; - long newLength = clean.length(); - entry.lengths[i] = newLength; - size = size - oldLength + newLength; - } - } else { - deleteIfExists(dirty); - } - } - - redundantOpCount++; - entry.currentEditor = null; - if (entry.readable | success) { - entry.readable = true; - journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); - if (success) { - entry.sequenceNumber = nextSequenceNumber++; - } - } else { - lruEntries.remove(entry.key); - journalWriter.write(REMOVE + ' ' + entry.key + '\n'); - } - journalWriter.flush(); - - if (size > maxSize || journalRebuildRequired()) { - executorService.submit(cleanupCallable); - } - } - - /** - * We only rebuild the journal when it will halve the size of the journal - * and eliminate at least 2000 ops. - */ - private boolean journalRebuildRequired() { - final int redundantOpCompactThreshold = 2000; - return redundantOpCount >= redundantOpCompactThreshold // - && redundantOpCount >= lruEntries.size(); - } - - /** - * Drops the entry for {@code key} if it exists and can be removed. Entries - * actively being edited cannot be removed. - * - * @return true if an entry was removed. - */ - public synchronized boolean remove(String key) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (entry == null || entry.currentEditor != null) { - return false; - } - - for (int i = 0; i < valueCount; i++) { - File file = entry.getCleanFile(i); - if (file.exists() && !file.delete()) { - throw new IOException("failed to delete " + file); - } - size -= entry.lengths[i]; - entry.lengths[i] = 0; - } - - redundantOpCount++; - journalWriter.append(REMOVE + ' ').append(key).append(String.valueOf('\n')); - lruEntries.remove(key); - - if (journalRebuildRequired()) { - executorService.submit(cleanupCallable); - } - - return true; - } - - /** - * Returns true if this cache has been closed. - */ - public synchronized boolean isClosed() { - return journalWriter == null; - } - - private void checkNotClosed() { - if (journalWriter == null) { - throw new IllegalStateException("cache is closed"); - } - } - - /** - * Force buffered operations to the filesystem. - */ - public synchronized void flush() throws IOException { - checkNotClosed(); - trimToSize(); - journalWriter.flush(); - } - - /** - * Closes this cache. Stored values will remain on the filesystem. - */ - public synchronized void close() throws IOException { - if (journalWriter == null) { - return; // Already closed. - } - for (Entry entry : new ArrayList<>(lruEntries.values())) { - if (entry.currentEditor != null) { - entry.currentEditor.abort(); - } - } - trimToSize(); - journalWriter.close(); - journalWriter = null; - } - - private void trimToSize() throws IOException { - while (size > maxSize) { - Map.Entry toEvict = lruEntries.entrySet().iterator().next(); - remove(toEvict.getKey()); - } - } - - /** - * Closes the cache and deletes all of its stored values. This will delete - * all files in the cache directory including files that weren't created by - * the cache. - */ - public void delete() throws IOException { - close(); - Util.deleteContents(directory); - } - - private void validateKey(String key) { - Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); - if (!matcher.matches()) { - throw new IllegalArgumentException("keys must match regex " - + STRING_KEY_PATTERN + ": \"" + key + "\""); - } - } - - /** - * A snapshot of the values for an entry. - */ - public final class Snapshot implements Closeable { - private final String key; - private final long sequenceNumber; - private final InputStream[] ins; - private final long[] lengths; - - private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) { - this.key = key; - this.sequenceNumber = sequenceNumber; - this.ins = ins; - this.lengths = lengths; - } - - /** - * Returns an editor for this snapshot's entry, or null if either the - * entry has changed since this snapshot was created or if another edit - * is in progress. - */ - public Editor edit() throws IOException { - return DiskLruCache.this.edit(key, sequenceNumber); - } - - /** - * Returns the unbuffered stream with the value for {@code index}. - */ - public InputStream getInputStream(int index) { - return ins[index]; - } - - /** - * Returns the string value for {@code index}. - */ - public String getString(int index) throws IOException { - return inputStreamToString(getInputStream(index)); - } - - /** - * Returns the byte length of the value for {@code index}. - */ - public long getLength(int index) { - return lengths[index]; - } - - public void close() { - for (InputStream in : ins) { - Util.closeQuietly(in); - } - } - } - - /** - * Edits the values for an entry. - */ - public final class Editor { - private final Entry entry; - private final boolean[] written; - private boolean hasErrors; - private boolean committed; - - private Editor(Entry entry) { - this.entry = entry; - this.written = (entry.readable) ? null : new boolean[valueCount]; - } - - /** - * Returns an unbuffered input stream to read the last committed value, - * or null if no value has been committed. - */ - public InputStream newInputStream(int index) { - synchronized (DiskLruCache.this) { - if (entry.currentEditor != this) { - throw new IllegalStateException(); - } - if (!entry.readable) { - return null; - } - try { - return new FileInputStream(entry.getCleanFile(index)); - } catch (FileNotFoundException e) { - return null; - } - } - } - - /** - * Returns the last committed value as a string, or null if no value - * has been committed. - */ - public String getString(int index) throws IOException { - InputStream in = newInputStream(index); - return in != null ? inputStreamToString(in) : null; - } - - /** - * Returns a new unbuffered output stream to write the value at - * {@code index}. If the underlying output stream encounters errors - * when writing to the filesystem, this edit will be aborted when - * {@link #commit} is called. The returned output stream does not throw - * IOExceptions. - */ - public OutputStream newOutputStream(int index) throws IOException { - if (index < 0 || index >= valueCount) { - throw new IllegalArgumentException("Expected index " + index + " to " - + "be greater than 0 and less than the maximum value count " - + "of " + valueCount); - } - synchronized (DiskLruCache.this) { - if (entry.currentEditor != this) { - throw new IllegalStateException(); - } - if (!entry.readable) { - written[index] = true; - } - File dirtyFile = entry.getDirtyFile(index); - FileOutputStream outputStream; - try { - outputStream = new FileOutputStream(dirtyFile); - } catch (FileNotFoundException e) { - // Attempt to recreate the cache directory. - directory.mkdirs(); - try { - outputStream = new FileOutputStream(dirtyFile); - } catch (FileNotFoundException e2) { - // We are unable to recover. Silently eat the writes. - return NULL_OUTPUT_STREAM; - } - } - return new FaultHidingOutputStream(outputStream); - } - } - - /** - * Sets the value at {@code index} to {@code value}. - */ - public void set(int index, String value) throws IOException { - Writer writer = null; - try { - writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8); - writer.write(value); - } finally { - Util.closeQuietly(writer); - } - } - - /** - * Commits this edit so it is visible to readers. This releases the - * edit lock so another edit may be started on the same key. - */ - public void commit() throws IOException { - if (hasErrors) { - completeEdit(this, false); - remove(entry.key); // The previous entry is stale. - } else { - completeEdit(this, true); - } - committed = true; - } - - /** - * Aborts this edit. This releases the edit lock so another edit may be - * started on the same key. - */ - public void abort() throws IOException { - completeEdit(this, false); - } - - public void abortUnlessCommitted() { - if (!committed) { - try { - abort(); - } catch (IOException ignored) { - } - } - } - - private class FaultHidingOutputStream extends FilterOutputStream { - private FaultHidingOutputStream(OutputStream out) { - super(out); - } - - @Override - public void write(int oneByte) { - try { - out.write(oneByte); - } catch (IOException e) { - hasErrors = true; - } - } - - @Override - public void write(byte[] buffer, int offset, int length) { - try { - out.write(buffer, offset, length); - } catch (IOException e) { - hasErrors = true; - } - } - - @Override - public void close() { - try { - out.close(); - } catch (IOException e) { - hasErrors = true; - } - } - - @Override - public void flush() { - try { - out.flush(); - } catch (IOException e) { - hasErrors = true; - } - } - } - } - - private final class Entry { - private final String key; - - /** - * Lengths of this entry's files. - */ - private final long[] lengths; - - /** - * True if this entry has ever been published. - */ - private boolean readable; - - /** - * The ongoing edit or null if this entry is not being edited. - */ - private Editor currentEditor; - - /** - * The sequence number of the most recently committed edit to this entry. - */ - private long sequenceNumber; - - private Entry(String key) { - this.key = key; - this.lengths = new long[valueCount]; - } - - public String getLengths() { - StringBuilder result = new StringBuilder(); - for (long size : lengths) { - result.append(' ').append(size); - } - return result.toString(); - } - - /** - * Set lengths using decimal numbers like "10123". - */ - private void setLengths(String[] strings) throws IOException { - if (strings.length != valueCount) { - throw invalidLengths(strings); - } - - try { - for (int i = 0; i < strings.length; i++) { - lengths[i] = Long.parseLong(strings[i]); - } - } catch (NumberFormatException e) { - throw invalidLengths(strings); - } - } - - private IOException invalidLengths(String[] strings) throws IOException { - throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); - } - - public File getCleanFile(int i) { - return new File(directory, key + "." + i); - } - - public File getDirtyFile(int i) { - return new File(directory, key + "." + i + ".tmp"); - } - } - - - - -} diff --git a/app/src/main/java/com/hippo/beerbelly/SimpleDiskCache.java b/app/src/main/java/com/hippo/beerbelly/SimpleDiskCache.java deleted file mode 100644 index 6813316d3..000000000 --- a/app/src/main/java/com/hippo/beerbelly/SimpleDiskCache.java +++ /dev/null @@ -1,504 +0,0 @@ -/* - * Copyright 2015-2016 Hippo Seven - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.beerbelly; - -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.hippo.streampipe.InputStreamPipe; -import com.hippo.streampipe.OutputStreamPipe; -import com.hippo.util.HashCodeUtils; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -public class SimpleDiskCache { - - private static final String TAG = SimpleDiskCache.class.getSimpleName(); - - /** - * Disk cache is free - */ - private static final int STATE_DISK_CACHE_NONE = 0; - - /** - * Disk cache is flushing or clearing - */ - private static final int STATE_DISK_CACHE_IN_USE = 1; - - /** - * Disk cache is reading or writing - */ - private static final int STATE_DISK_CACHE_BUSY = 2; - - private final File mCacheDir; - private final int mSize; - @NonNull - private final Map mLockMap = new HashMap<>(); - @NonNull - private final LockPool mLockPool = new LockPool(); - private int mDiskCacheState; - @Nullable - private DiskLruCache mDiskLruCache; - - public SimpleDiskCache(File cacheDir, int size) { - mCacheDir = cacheDir; - mSize = size; - - try { - init(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - /** - * A hashing method that changes a string (like a URL) into a hash suitable - * for using as a disk filename. - * - * @param key The key used to store the file - */ - private static String hashKeyForDisk(final String key) { - String cacheKey; - try { - final MessageDigest digest = MessageDigest.getInstance("MD5"); - digest.update(key.getBytes()); - cacheKey = HashCodeUtils.bytesToHexString(digest.digest()); - } catch (final NoSuchAlgorithmException e) { - cacheKey = String.valueOf(key.hashCode()); - } - return cacheKey; - } - - public synchronized boolean isValid() { - return mDiskLruCache != null; - } - - private synchronized void init() throws IOException { - if (!isValid()) { - mDiskLruCache = DiskLruCache.open(mCacheDir, 1, 1, mSize); - } - } - - public synchronized long size() { - if (null != mDiskLruCache) { - return mDiskLruCache.size(); - } else { - return -1L; - } - } - - public synchronized void flush() { - // Wait for cache available - while (mDiskCacheState != STATE_DISK_CACHE_NONE) { - try { - wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - mDiskCacheState = STATE_DISK_CACHE_BUSY; - - if (null != mDiskLruCache) { - try { - mDiskLruCache.flush(); - } catch (IOException e) { - Log.e(TAG, "SimpleDiskCache flush", e); - } - } - - mDiskCacheState = STATE_DISK_CACHE_NONE; - notifyAll(); - } - - public synchronized boolean clear() { - boolean result = true; - if (null == mDiskLruCache) { - return false; - } - - // Wait for cache available - while (mDiskCacheState != STATE_DISK_CACHE_NONE) { - try { - wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - mDiskCacheState = STATE_DISK_CACHE_BUSY; - - try { - mDiskLruCache.delete(); - } catch (IOException e) { - Log.e(TAG, "SimpleDiskCache clearCache", e); - } - mDiskLruCache = null; - try { - init(); - } catch (IOException e) { - Log.e(TAG, "SimpleDiskCache init", e); - result = false; - } - - mDiskCacheState = STATE_DISK_CACHE_NONE; - notifyAll(); - return result; - } - - private synchronized CounterLock obtainLock(String key) { - // Wait for clear over - while (mDiskCacheState == STATE_DISK_CACHE_BUSY) { - try { - wait(); - } catch (InterruptedException e) { - // Empty - } - } - mDiskCacheState = STATE_DISK_CACHE_IN_USE; - - // Get lock from key - CounterLock lock; - lock = mLockMap.get(key); - if (lock == null) { - lock = mLockPool.get(); - mLockMap.put(key, lock); - } - lock.obtain(); - - return lock; - } - - private synchronized void releaseLock(String key, CounterLock lock) { - lock.release(); - if (lock.isFree()) { - mLockMap.remove(key); - mLockPool.push(lock); - } - - if (mLockMap.isEmpty()) { - mDiskCacheState = STATE_DISK_CACHE_NONE; - notifyAll(); - } - } - - public boolean contain(@NonNull String key) { - String diskKey = hashKeyForDisk(key); - CounterLock lock = obtainLock(diskKey); - if (null == mDiskLruCache) { - releaseLock(diskKey, lock); - return false; - } - lock.readLock().lock(); - - boolean result = mDiskLruCache.contain(diskKey); - - lock.readLock().unlock(); - releaseLock(diskKey, lock); - - return result; - } - - public boolean remove(@NonNull String key) { - String diskKey = hashKeyForDisk(key); - CounterLock lock = obtainLock(diskKey); - if (null == mDiskLruCache) { - releaseLock(diskKey, lock); - return false; - } - lock.writeLock().lock(); - - boolean result; - try { - result = mDiskLruCache.remove(diskKey); - } catch (IOException e) { - e.printStackTrace(); - result = false; - } - - lock.writeLock().unlock(); - releaseLock(diskKey, lock); - - return result; - } - - public boolean put(@NonNull String key, @NonNull InputStream is) { - String diskKey = hashKeyForDisk(key); - CounterLock lock = obtainLock(diskKey); - if (null == mDiskLruCache) { - releaseLock(diskKey, lock); - return false; - } - lock.writeLock().lock(); - - boolean result = false; - if (isValid()) { - result = putToDisk(diskKey, is); - } - - lock.writeLock().unlock(); - releaseLock(diskKey, lock); - - return result; - } - - private boolean putToDisk(String key, InputStream is) { - DiskLruCache.Editor editor = null; - OutputStream os = null; - boolean completeEdit = false; - try { - //noinspection ConstantConditions - editor = mDiskLruCache.edit(key); - if (editor == null) { - // The editor is in progress - return false; - } - - os = editor.newOutputStream(0); - if (os != null) { - Util.copy(is, os); - completeEdit = true; - editor.commit(); - return true; - } else { - // Can't get OutputStream - completeEdit = true; - editor.abort(); - return false; - } - } catch (IOException e) { - Util.closeQuietly(os); - try { - if (!completeEdit && editor != null) { - editor.abort(); - } - } catch (IOException ignored) { - } - return false; - } - } - - /** - * @param key the key of the target - * @return the InputStreamPipe, null for missing - */ - @Nullable - public InputStreamPipe getInputStreamPipe(@NonNull String key) { - if (contain(key)) { - String diskKey = hashKeyForDisk(key); - return new CacheInputStreamPipe(diskKey); - } else { - return null; - } - } - - @NonNull - public OutputStreamPipe getOutputStreamPipe(@NonNull String key) { - String diskKey = hashKeyForDisk(key); - return new CacheOutputStreamPipe(diskKey); - } - - private class CacheInputStreamPipe implements InputStreamPipe { - - private final String mKey; - private CounterLock mLock; - private DiskLruCache.Snapshot mCurrentSnapshot; - - private CacheInputStreamPipe(String key) { - mKey = key; - } - - @Override - public void obtain() { - if (mLock == null) { - mLock = obtainLock(mKey); - mLock.readLock().lock(); - } - } - - @Override - public void release() { - if (mCurrentSnapshot != null) { - throw new IllegalStateException("Please close it first"); - } - - if (mLock != null) { - mLock.readLock().unlock(); - releaseLock(mKey, mLock); - mLock = null; - } - } - - @Override - public @NonNull - InputStream open() throws IOException { - if (mLock == null) { - throw new IllegalStateException("Please obtain it first"); - } - if (mCurrentSnapshot != null) { - throw new IllegalStateException("Please close it before reopen"); - } - if (null == mDiskLruCache) { - throw new IOException("Can't find disk lru cache"); - } - - DiskLruCache.Snapshot snapshot; - snapshot = mDiskLruCache.get(mKey); - if (snapshot == null) { - throw new IOException("Miss the key " + mKey); - } - mCurrentSnapshot = snapshot; - return snapshot.getInputStream(0); - } - - @Override - public void close() { - Util.closeQuietly(mCurrentSnapshot); - mCurrentSnapshot = null; - } - } - - private class CacheOutputStreamPipe implements OutputStreamPipe { - - private final String mKey; - private CounterLock mLock; - private DiskLruCache.Editor mCurrentEditor; - - private CacheOutputStreamPipe(String key) { - mKey = key; - } - - @Override - public void obtain() { - if (mLock == null) { - mLock = obtainLock(mKey); - mLock.writeLock().lock(); - } - } - - @Override - public void release() { - if (mCurrentEditor != null) { - throw new IllegalStateException("Please close it first"); - } - - if (mLock != null) { - mLock.writeLock().unlock(); - releaseLock(mKey, mLock); - mLock = null; - } - } - - @Override - public @NonNull - OutputStream open() throws IOException { - if (mLock == null) { - throw new IllegalStateException("Please obtain it first"); - } - if (mCurrentEditor != null) { - throw new IllegalStateException("Please close it before reopen"); - } - if (null == mDiskLruCache) { - throw new IOException("Can't find disk lru cache"); - } - - DiskLruCache.Editor editor = mDiskLruCache.edit(mKey); - if (editor == null) { - throw new IOException("Miss the key " + mKey); - } - - OutputStream os; - try { - os = editor.newOutputStream(0); - mCurrentEditor = editor; - return os; - } catch (IOException e) { - // Get trouble - try { - editor.abort(); - } catch (IOException ex) { - // Ignore - } - throw e; - } - } - - @Override - public void close() { - if (mCurrentEditor != null) { - try { - mCurrentEditor.commit(); - } catch (IOException e) { - // Empty - } - mCurrentEditor = null; - } - } - } - - private class CounterLock extends ReentrantReadWriteLock { - - private int mCount; - - public boolean isFree() { - return mCount == 0; - } - - public void release() { - if (--mCount < 0) { - throw new IllegalStateException("Release time is more than occupy time"); - } - } - - public void obtain() { - ++mCount; - } - } - - private class LockPool { - - private static final int MAX_SIZE = 10; - - private final CounterLock[] mArray = new CounterLock[MAX_SIZE]; - private final int mMaxSize = MAX_SIZE; - private int mSize; - - public void push(CounterLock lock) { - if (lock != null && mSize < mMaxSize) { - mArray[mSize++] = lock; - } - } - - public CounterLock get() { - if (mSize > 0) { - CounterLock lock = mArray[--mSize]; - mArray[mSize] = null; - return lock; - } else { - return new CounterLock(); - } - } - } -} diff --git a/app/src/main/java/com/hippo/beerbelly/StrictLineReader.java b/app/src/main/java/com/hippo/beerbelly/StrictLineReader.java deleted file mode 100644 index 9f7c885d0..000000000 --- a/app/src/main/java/com/hippo/beerbelly/StrictLineReader.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright (C) 2012 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.beerbelly; - -import androidx.annotation.NonNull; - -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; - -/** - * Buffers input from an {@link InputStream} for reading lines. - * - *

This class is used for buffered reading of lines. For purposes of this class, a line ends - * with "\n" or "\r\n". End of input is reported by throwing {@code EOFException}. Unterminated - * line at end of input is invalid and will be ignored, the caller may use {@code - * hasUnterminatedLine()} to detect it after catching the {@code EOFException}. - * - *

This class is intended for reading input that strictly consists of lines, such as line-based - * cache entries or cache journal. Unlike the {@link java.io.BufferedReader} which in conjunction - * with {@link java.io.InputStreamReader} provides similar functionality, this class uses different - * end-of-input reporting and a more restrictive definition of a line. - * - *

This class supports only charsets that encode '\r' and '\n' as a single byte with value 13 - * and 10, respectively, and the representation of no other character contains these values. - * We currently check in constructor that the charset is one of US-ASCII, UTF-8 and ISO-8859-1. - * The default charset is US_ASCII. - */ -class StrictLineReader implements Closeable { - private static final byte CR = (byte) '\r'; - private static final byte LF = (byte) '\n'; - - private final InputStream in; - private final Charset charset; - - /* - * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end - * and the data in the range [pos, end) is buffered for reading. At end of input, if there is - * an unterminated line, we set end == -1, otherwise end == pos. If the underlying - * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. - */ - private byte[] buf; - private int pos; - private int end; - - /** - * Constructs a new {@code LineReader} with the specified charset and the default capacity. - * - * @param in the {@code InputStream} to read data from. - * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are - * supported. - * @throws NullPointerException if {@code in} or {@code charset} is null. - * @throws IllegalArgumentException if the specified charset is not supported. - */ - public StrictLineReader(InputStream in, Charset charset) { - this(in, 8192, charset); - } - - /** - * Constructs a new {@code LineReader} with the specified capacity and charset. - * - * @param in the {@code InputStream} to read data from. - * @param capacity the capacity of the buffer. - * @param charset the charset used to decode data. Only US-ASCII, UTF-8 and ISO-8859-1 are - * supported. - * @throws NullPointerException if {@code in} or {@code charset} is null. - * @throws IllegalArgumentException if {@code capacity} is negative or zero - * or the specified charset is not supported. - */ - public StrictLineReader(InputStream in, int capacity, Charset charset) { - if (in == null || charset == null) { - throw new NullPointerException(); - } - if (capacity < 0) { - throw new IllegalArgumentException("capacity <= 0"); - } - if (!(charset.equals(Util.US_ASCII))) { - throw new IllegalArgumentException("Unsupported encoding"); - } - - this.in = in; - this.charset = charset; - buf = new byte[capacity]; - } - - /** - * Closes the reader by closing the underlying {@code InputStream} and - * marking this reader as closed. - * - * @throws IOException for errors when closing the underlying {@code InputStream}. - */ - public void close() throws IOException { - synchronized (in) { - if (buf != null) { - buf = null; - in.close(); - } - } - } - - /** - * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"}, - * this end of line marker is not included in the result. - * - * @return the next line from the input. - * @throws IOException for underlying {@code InputStream} errors. - * @throws EOFException for the end of source stream. - */ - public String readLine() throws IOException { - synchronized (in) { - if (buf == null) { - throw new IOException("LineReader is closed"); - } - - // Read more data if we are at the end of the buffered data. - // Though it's an error to read after an exception, we will let {@code fillBuf()} - // throw again if that happens; thus we need to handle end == -1 as well as end == pos. - if (pos >= end) { - fillBuf(); - } - // Try to find LF in the buffered data and return the line if successful. - for (int i = pos; i != end; ++i) { - if (buf[i] == LF) { - int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i; - String res = new String(buf, pos, lineEnd - pos, charset); - pos = i + 1; - return res; - } - } - - // Let's anticipate up to 80 characters on top of those already read. - ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) { - @NonNull - @Override - public String toString() { - int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count; - return new String(buf, 0, length, charset); - } - }; - - while (true) { - out.write(buf, pos, end - pos); - // Mark unterminated line in case fillBuf throws EOFException or IOException. - end = -1; - fillBuf(); - // Try to find LF in the buffered data and return the line if successful. - for (int i = pos; i != end; ++i) { - if (buf[i] == LF) { - if (i != pos) { - out.write(buf, pos, i - pos); - } - pos = i + 1; - return out.toString(); - } - } - } - } - } - - public boolean hasUnterminatedLine() { - return end == -1; - } - - /** - * Reads new input data into the buffer. Call only with pos == end or end == -1, - * depending on the desired outcome if the function throws. - */ - private void fillBuf() throws IOException { - int result = in.read(buf, 0, buf.length); - if (result == -1) { - throw new EOFException(); - } - pos = 0; - end = result; - } -} diff --git a/app/src/main/java/com/hippo/beerbelly/Util.java b/app/src/main/java/com/hippo/beerbelly/Util.java deleted file mode 100644 index 25c70ddfc..000000000 --- a/app/src/main/java/com/hippo/beerbelly/Util.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2010 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hippo.beerbelly; - -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Reader; -import java.io.StringWriter; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; - -/** - * Junk drawer of utility methods. - */ -final class Util { - static final Charset US_ASCII = StandardCharsets.US_ASCII; - static final Charset UTF_8 = StandardCharsets.UTF_8; - private static final int EOF = -1; - private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; - - private Util() { - } - - static String readFully(Reader reader) throws IOException { - try (reader) { - StringWriter writer = new StringWriter(); - char[] buffer = new char[1024]; - int count; - while ((count = reader.read(buffer)) != -1) { - writer.write(buffer, 0, count); - } - return writer.toString(); - } - } - - /** - * Deletes the contents of {@code dir}. Throws an IOException if any file - * could not be deleted, or if {@code dir} is not a readable directory. - */ - static void deleteContents(File dir) throws IOException { - File[] files = dir.listFiles(); - if (files == null) { - throw new IOException("not a readable directory: " + dir); - } - for (File file : files) { - if (file.isDirectory()) { - deleteContents(file); - } - if (!file.delete()) { - throw new IOException("failed to delete file: " + file); - } - } - } - - static void closeQuietly(/*Auto*/Closeable closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (RuntimeException rethrown) { - throw rethrown; - } catch (Exception ignored) { - } - } - } - - /** - * Copy bytes from an InputStream to an - * OutputStream. - * - * @param input the InputStream - * @param output the OutputStream - * @return the number of bytes copied - */ - public static long copy(InputStream input, OutputStream output) - throws IOException { - byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; - long count = 0; - int n; - while (EOF != (n = input.read(buffer))) { - output.write(buffer, 0, n); - count += n; - } - return count; - } -} diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java b/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java index bb7594312..e2001cb39 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.java @@ -17,12 +17,10 @@ package com.hippo.ehviewer.spider; import android.content.Context; -import android.graphics.BitmapFactory; -import android.webkit.MimeTypeMap; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.hippo.beerbelly.SimpleDiskCache; import com.hippo.ehviewer.EhDB; import com.hippo.ehviewer.Settings; import com.hippo.ehviewer.client.EhCacheKeyFactory; @@ -40,14 +38,21 @@ import com.hippo.yorozuya.MathUtils; import com.hippo.yorozuya.Utilities; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.util.Locale; +import coil.disk.DiskCache; +import okio.BufferedSource; +import okio.Okio; + public final class SpiderDen { @Nullable - private static SimpleDiskCache sCache; + private static DiskCache sCache; // We use data to store image file, and metadata for image type private final GalleryInfo mGalleryInfo; private final long mGid; @Nullable @@ -61,8 +66,8 @@ public final class SpiderDen { } public static void initialize(Context context) { - sCache = new SimpleDiskCache(new File(context.getCacheDir(), "image"), - MathUtils.clamp(Settings.getReadCacheSize(), 40, 1280) * 1024 * 1024); + sCache = new DiskCache.Builder().directory(new File(context.getCacheDir(), "gallery_image")) + .maxSizeBytes((long) MathUtils.clamp(Settings.getReadCacheSize(), 40, 1280) * 1024 * 1024).build(); } public static UniFile getGalleryDownloadDir(long gid) { @@ -137,7 +142,10 @@ public final class SpiderDen { } String key = EhCacheKeyFactory.getImageKey(mGid, index); - return sCache.contain(key); + var snapshot = sCache.get(key); + var exist = snapshot != null; + if (exist) snapshot.close(); + return exist; } private boolean containInDownloadDir(int index) { @@ -171,42 +179,27 @@ public final class SpiderDen { } // Find image file in cache String key = EhCacheKeyFactory.getImageKey(mGid, index); - InputStreamPipe pipe = sCache.getInputStreamPipe(key); - if (pipe == null) { + var snapshot = sCache.get(key); + if (snapshot == null) { return false; } OutputStream os = null; - try { + try (snapshot; FileInputStream is = new FileInputStream(snapshot.getData().toFile()); BufferedSource buf = Okio.buffer(Okio.source(snapshot.getMetadata().toFile()))) { // Get extension - String extension; - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - pipe.obtain(); - BitmapFactory.decodeStream(pipe.open(), null, options); - pipe.close(); - extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(options.outMimeType); - if (extension != null) { - extension = '.' + extension; - } else { - return false; - } - // Fix extension - extension = fixExtension(extension); + String extension = fixExtension("." + buf.readUtf8Line()); // Copy from cache to download dir UniFile file = dir.createFile(generateImageFilename(index, extension)); if (file == null) { return false; } os = file.openOutputStream(); - IOUtils.copy(pipe.open(), os); + IOUtils.copy(is, os); return true; } catch (IOException e) { return false; } finally { IOUtils.closeQuietly(os); - pipe.close(); - pipe.release(); } } @@ -253,13 +246,39 @@ public final class SpiderDen { } @Nullable - private OutputStreamPipe openCacheOutputStreamPipe(int index) { + private OutputStreamPipe openCacheOutputStreamPipe(int index, @Nullable String extension) { if (sCache == null) { return null; } String key = EhCacheKeyFactory.getImageKey(mGid, index); - return sCache.getOutputStreamPipe(key); + var editor = sCache.edit(key); + if (editor == null) return null; + try (var sink = Okio.buffer(Okio.sink(editor.getMetadata().toFile()))) { + sink.writeUtf8(extension); + } catch (IOException e) { + e.printStackTrace(); + } + return new OutputStreamPipe() { + @Override + public void obtain() { + } + + @Override + public void release() { + } + + @NonNull + @Override + public OutputStream open() throws IOException { + return new FileOutputStream(editor.getData().toFile()); + } + + @Override + public void close() { + editor.commit(); + } + }; } /** @@ -287,7 +306,7 @@ public final class SpiderDen { // Return the download pipe is the gallery has been downloaded OutputStreamPipe pipe = openDownloadOutputStreamPipe(index, extension); if (pipe == null) { - pipe = openCacheOutputStreamPipe(index); + pipe = openCacheOutputStreamPipe(index, extension); } return pipe; } else if (mMode == SpiderQueen.MODE_DOWNLOAD) { @@ -304,7 +323,30 @@ public final class SpiderDen { } String key = EhCacheKeyFactory.getImageKey(mGid, index); - return sCache.getInputStreamPipe(key); + var snapshot = sCache.get(key); + if (snapshot == null) return null; + return new InputStreamPipe() { + @Override + public void obtain() { + + } + + @Override + public void release() { + + } + + @NonNull + @Override + public InputStream open() throws IOException { + return new FileInputStream(snapshot.getData().toFile()); + } + + @Override + public void close() { + snapshot.close(); + } + }; } @Nullable diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java index 67999f0c2..9b228d954 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderQueen.java @@ -46,7 +46,6 @@ import com.hippo.ehviewer.client.parser.GalleryMultiPageViewerPTokenParser; import com.hippo.ehviewer.client.parser.GalleryPageApiParser; import com.hippo.ehviewer.client.parser.GalleryPageParser; import com.hippo.ehviewer.client.parser.GalleryPageUrlParser; -import com.hippo.ehviewer.gallery.PageLoader2; import com.hippo.image.Image; import com.hippo.streampipe.InputStreamPipe; import com.hippo.streampipe.OutputStreamPipe; @@ -102,7 +101,7 @@ public final class SpiderQueen implements Runnable { public static final String SPIDER_INFO_FILENAME = ".ehviewer"; private static final String TAG = SpiderQueen.class.getSimpleName(); private static final AtomicInteger sIdGenerator = new AtomicInteger(); - private static final boolean DEBUG_LOG = false; + private static final boolean DEBUG_LOG = true; private static final boolean DEBUG_PTOKEN = true; private static final String[] URL_509_SUFFIX_ARRAY = { "/509.gif", @@ -155,6 +154,8 @@ public final class SpiderQueen implements Runnable { // For download, when it go to mPageStateArray.size(), done private volatile int mDownloadPage = -1; + private boolean mStoped = false; + private SpiderQueen(@NonNull GalleryInfo galleryInfo) { mHttpClient = EhApplication.getOkHttpClient(); mSpiderInfoCache = EhApplication.getSpiderInfoCache(); @@ -387,10 +388,15 @@ public final class SpiderQueen implements Runnable { } private void stop() { - Thread queenThread = mQueenThread; - if (queenThread != null) { - queenThread.interrupt(); - mQueenThread = null; + mStoped = true; + synchronized (mQueenLock) { + mQueenLock.notifyAll(); + } + synchronized (mDecodeRequestQueue) { + mDecodeRequestQueue.notifyAll(); + } + synchronized (mWorkerLock) { + mWorkerLock.notifyAll(); } } @@ -875,8 +881,8 @@ public final class SpiderQueen implements Runnable { // Read spider info SpiderInfo spiderInfo = readSpiderInfoFromLocal(); - // Check interrupted - if (Thread.currentThread().isInterrupted()) { + // Check Stopped + if (mStoped) { return; } @@ -891,16 +897,16 @@ public final class SpiderQueen implements Runnable { } mSpiderInfo.lazySet(spiderInfo); - // Check interrupted - if (Thread.currentThread().isInterrupted()) { + // Check Stopped + if (mStoped) { return; } // Write spider info to file writeSpiderInfoToLocal(spiderInfo); - // Check interrupted - if (Thread.currentThread().isInterrupted()) { + // Check Stopped + if (mStoped) { return; } @@ -924,17 +930,19 @@ public final class SpiderQueen implements Runnable { } // handle pToken request - while (!Thread.currentThread().isInterrupted()) { + while (!mStoped) { Integer index = mRequestPTokenQueue.poll(); if (index == null) { // No request index, wait here synchronized (mQueenLock) { + if (mStoped) break; try { mQueenLock.wait(); } catch (InterruptedException e) { break; } + if (mStoped) break; } continue; } @@ -989,16 +997,9 @@ public final class SpiderQueen implements Runnable { // Set mQueenThread null mQueenThread = null; - // Interrupt decoder - for (Thread decoderThread : mDecodeThreadArray) { - if (decoderThread != null) { - decoderThread.interrupt(); - } - } - - // Interrupt all workers + // Stop all workers synchronized (mWorkerLock) { - mWorkerPoolExecutor.shutdownNow(); + mWorkerPoolExecutor.shutdown(); mWorkerPoolExecutor = null; } notifyFinish(); @@ -1146,7 +1147,6 @@ public final class SpiderQueen implements Runnable { String pageUrl = null; String error = null; boolean forceHtml = false; - boolean interrupt = false; boolean leakSkipHathKey = false; for (int i = 0; i < 5; i++) { @@ -1191,10 +1191,9 @@ public final class SpiderQueen implements Runnable { break; } - // Check interrupted - if (Thread.currentThread().isInterrupted()) { + // Check Stopped + if (mStoped) { error = "Interrupted"; - interrupt = true; break; } } @@ -1226,10 +1225,9 @@ public final class SpiderQueen implements Runnable { } } - // Check interrupted - if (Thread.currentThread().isInterrupted()) { + // Check Stopped + if (mStoped) { error = "Interrupted"; - interrupt = true; break; } } @@ -1296,7 +1294,7 @@ public final class SpiderQueen implements Runnable { final byte[] data = new byte[1024 * 4]; long receivedSize = 0; - while (!Thread.currentThread().isInterrupted()) { + while (!mStoped) { int bytesRead = is.read(data); if (bytesRead == -1) { response.close(); @@ -1373,9 +1371,8 @@ public final class SpiderQueen implements Runnable { } } - // Check interrupted - if (Thread.currentThread().isInterrupted()) { - interrupt = true; + // Check Stopped + if (mStoped) { error = "Interrupted"; break; } @@ -1391,13 +1388,11 @@ public final class SpiderQueen implements Runnable { } catch (InterruptedException e) { e.printStackTrace(); } - Thread.sleep(0); // to make allow to catch InterruptedException return true; } catch (IOException e) { e.printStackTrace(); error = GetText.getString(R.string.error_socket); forceHtml = true; - } catch (InterruptedException ignored) { } finally { IOUtils.closeQuietly(is); @@ -1411,7 +1406,7 @@ public final class SpiderQueen implements Runnable { mSpiderDen.remove(index); updatePageState(index, STATE_FAILED, error); - return !interrupt; + return !mStoped; } // false for stop @@ -1482,7 +1477,7 @@ public final class SpiderQueen implements Runnable { String pToken = null; // Get token - while (!Thread.currentThread().isInterrupted()) { + while (!mStoped) { synchronized (mPTokenLock) { pToken = spiderInfo.pTokenMap.get(index); } @@ -1494,6 +1489,7 @@ public final class SpiderQueen implements Runnable { } // Wait synchronized (mWorkerLock) { + if (mStoped) break; try { mWorkerLock.wait(); } catch (InterruptedException e) { @@ -1503,6 +1499,7 @@ public final class SpiderQueen implements Runnable { } break; } + if (mStoped) break; } } else { break; @@ -1519,7 +1516,7 @@ public final class SpiderQueen implements Runnable { String previousPToken = null; int previousIndex = index - 1; // Get token - while (previousIndex >= 0 && !Thread.currentThread().isInterrupted()) { + while (previousIndex >= 0 && !mStoped) { synchronized (mPTokenLock) { previousPToken = spiderInfo.pTokenMap.get(previousIndex); } @@ -1531,6 +1528,7 @@ public final class SpiderQueen implements Runnable { } // Wait synchronized (mWorkerLock) { + if (mStoped) break; try { mWorkerLock.wait(); } catch (InterruptedException e) { @@ -1540,6 +1538,7 @@ public final class SpiderQueen implements Runnable { } break; } + if (mStoped) break; } } else { break; @@ -1563,8 +1562,7 @@ public final class SpiderQueen implements Runnable { Log.i(TAG, Thread.currentThread().getName() + ": start"); } - while (mSpiderDen.isReady() && !Thread.currentThread().isInterrupted() && runInternal()) - ; + while (mSpiderDen.isReady() && !mStoped && runInternal()) ; boolean finish; // Clear in spider worker array @@ -1607,9 +1605,10 @@ public final class SpiderQueen implements Runnable { Log.i(TAG, Thread.currentThread().getName() + ": start"); } - while (!Thread.currentThread().isInterrupted()) { + while (!mStoped) { int index; synchronized (mDecodeRequestQueue) { + if (mStoped) break; if (mDecodeRequestQueue.isEmpty()) { try { mDecodeRequestQueue.wait(); @@ -1617,6 +1616,7 @@ public final class SpiderQueen implements Runnable { // Interrupted break; } + if (mStoped) break; continue; } index = mDecodeRequestQueue.remove();