The cache stores its data in a directory on the filesystem. This
64 | * directory must be exclusive to the cache; the cache may delete or overwrite
65 | * files from its directory. It is an error for multiple processes to use the
66 | * same cache directory at the same time.
67 | *
68 | *
This cache limits the number of bytes that it will store on the
69 | * filesystem. When the number of stored bytes exceeds the limit, the cache will
70 | * remove entries in the background until the limit is satisfied. The limit is
71 | * not strict: the cache may temporarily exceed it while waiting for files to be
72 | * deleted. The limit does not include filesystem overhead or the cache
73 | * journal so space-sensitive applications should set a conservative limit.
74 | *
75 | *
Clients call {@link #edit} to create or update the values of an entry. An
76 | * entry may have only one editor at one time; if a value is not available to be
77 | * edited then {@link #edit} will return null.
78 | *
79 | *
When an entry is being created it is necessary to
80 | * supply a full set of values; the empty value should be used as a
81 | * placeholder if necessary.
82 | *
When an entry is being edited, it is not necessary
83 | * to supply data for every value; values default to their previous
84 | * value.
85 | *
86 | * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
87 | * or {@link Editor#abort}. Committing is atomic: a read observes the full set
88 | * of values as they were before or after the commit, but never a mix of values.
89 | *
90 | *
Clients call {@link #get} to read a snapshot of an entry. The read will
91 | * observe the value at the time that {@link #get} was called. Updates and
92 | * removals after the call do not impact ongoing reads.
93 | *
94 | *
This class is tolerant of some I/O errors. If files are missing from the
95 | * filesystem, the corresponding entries will be dropped from the cache. If
96 | * an error occurs while writing a cache value, the edit will fail silently.
97 | * Callers should handle other problems by catching {@code IOException} and
98 | * responding appropriately.
99 | */
100 | public final class DiskLruCache implements Closeable {
101 | static final String JOURNAL_FILE = "journal";
102 | static final String JOURNAL_FILE_TMP = "journal.tmp";
103 | static final String MAGIC = "libcore.io.DiskLruCache";
104 | static final String VERSION_1 = "1";
105 | static final long ANY_SEQUENCE_NUMBER = -1;
106 | private static final String CLEAN = "CLEAN";
107 | private static final String DIRTY = "DIRTY";
108 | private static final String REMOVE = "REMOVE";
109 | private static final String READ = "READ";
110 |
111 | private static final Charset UTF_8 = Charset.forName("UTF-8");
112 | private static final int IO_BUFFER_SIZE = 8 * 1024;
113 |
114 | /*
115 | * This cache uses a journal file named "journal". A typical journal file
116 | * looks like this:
117 | * libcore.io.DiskLruCache
118 | * 1
119 | * 100
120 | * 2
121 | *
122 | * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
123 | * DIRTY 335c4c6028171cfddfbaae1a9c313c52
124 | * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
125 | * REMOVE 335c4c6028171cfddfbaae1a9c313c52
126 | * DIRTY 1ab96a171faeeee38496d8b330771a7a
127 | * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
128 | * READ 335c4c6028171cfddfbaae1a9c313c52
129 | * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
130 | *
131 | * The first five lines of the journal form its header. They are the
132 | * constant string "libcore.io.DiskLruCache", the disk cache's version,
133 | * the application's version, the value count, and a blank line.
134 | *
135 | * Each of the subsequent lines in the file is a record of the state of a
136 | * cache entry. Each line contains space-separated values: a state, a key,
137 | * and optional state-specific values.
138 | * o DIRTY lines track that an entry is actively being created or updated.
139 | * Every successful DIRTY action should be followed by a CLEAN or REMOVE
140 | * action. DIRTY lines without a matching CLEAN or REMOVE indicate that
141 | * temporary files may need to be deleted.
142 | * o CLEAN lines track a cache entry that has been successfully published
143 | * and may be read. A publish line is followed by the lengths of each of
144 | * its values.
145 | * o READ lines track accesses for LRU.
146 | * o REMOVE lines track entries that have been deleted.
147 | *
148 | * The journal file is appended to as cache operations occur. The journal may
149 | * occasionally be compacted by dropping redundant lines. A temporary file named
150 | * "journal.tmp" will be used during compaction; that file should be deleted if
151 | * it exists when the cache is opened.
152 | */
153 |
154 | private final File directory;
155 | private final File journalFile;
156 | private final File journalFileTmp;
157 | private final int appVersion;
158 | private final long maxSize;
159 | private final int valueCount;
160 | private long size = 0;
161 | private Writer journalWriter;
162 | private final LinkedHashMap lruEntries
163 | = new LinkedHashMap(0, 0.75f, true);
164 | private int redundantOpCount;
165 |
166 | /**
167 | * To differentiate between old and current snapshots, each entry is given
168 | * a sequence number each time an edit is committed. A snapshot is stale if
169 | * its sequence number is not equal to its entry's sequence number.
170 | */
171 | private long nextSequenceNumber = 0;
172 |
173 | /* From java.util.Arrays */
174 | @SuppressWarnings("unchecked")
175 | private static T[] copyOfRange(T[] original, int start, int end) {
176 | final int originalLength = original.length; // For exception priority compatibility.
177 | if (start > end) {
178 | throw new IllegalArgumentException();
179 | }
180 | if (start < 0 || start > originalLength) {
181 | throw new ArrayIndexOutOfBoundsException();
182 | }
183 | final int resultLength = end - start;
184 | final int copyLength = Math.min(resultLength, originalLength - start);
185 | final T[] result = (T[]) Array
186 | .newInstance(original.getClass().getComponentType(), resultLength);
187 | System.arraycopy(original, start, result, 0, copyLength);
188 | return result;
189 | }
190 |
191 | /**
192 | * Returns the remainder of 'reader' as a string, closing it when done.
193 | */
194 | public static String readFully(Reader reader) throws IOException {
195 | try {
196 | StringWriter writer = new StringWriter();
197 | char[] buffer = new char[1024];
198 | int count;
199 | while ((count = reader.read(buffer)) != -1) {
200 | writer.write(buffer, 0, count);
201 | }
202 | return writer.toString();
203 | } finally {
204 | reader.close();
205 | }
206 | }
207 |
208 | /**
209 | * Returns the ASCII characters up to but not including the next "\r\n", or
210 | * "\n".
211 | *
212 | * @throws EOFException if the stream is exhausted before the next newline
213 | * character.
214 | */
215 | public static String readAsciiLine(InputStream in) throws IOException {
216 | // TODO: support UTF-8 here instead
217 |
218 | StringBuilder result = new StringBuilder(80);
219 | while (true) {
220 | int c = in.read();
221 | if (c == -1) {
222 | throw new EOFException();
223 | } else if (c == '\n') {
224 | break;
225 | }
226 |
227 | result.append((char) c);
228 | }
229 | int length = result.length();
230 | if (length > 0 && result.charAt(length - 1) == '\r') {
231 | result.setLength(length - 1);
232 | }
233 | return result.toString();
234 | }
235 |
236 | /**
237 | * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
238 | */
239 | public static void closeQuietly(Closeable closeable) {
240 | if (closeable != null) {
241 | try {
242 | closeable.close();
243 | } catch (RuntimeException rethrown) {
244 | throw rethrown;
245 | } catch (Exception ignored) {
246 | }
247 | }
248 | }
249 |
250 | /**
251 | * Recursively delete everything in {@code dir}.
252 | */
253 | // TODO: this should specify paths as Strings rather than as Files
254 | public static void deleteContents(File dir) throws IOException {
255 | File[] files = dir.listFiles();
256 | if (files == null) {
257 | throw new IllegalArgumentException("not a directory: " + dir);
258 | }
259 | for (File file : files) {
260 | if (file.isDirectory()) {
261 | deleteContents(file);
262 | }
263 | if (!file.delete()) {
264 | throw new IOException("failed to delete file: " + file);
265 | }
266 | }
267 | }
268 |
269 | /** This cache uses a single background thread to evict entries. */
270 | private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
271 | 60L, TimeUnit.SECONDS, new LinkedBlockingQueue());
272 | private final Callable cleanupCallable = new Callable() {
273 | @Override public Void call() throws Exception {
274 | synchronized (DiskLruCache.this) {
275 | if (journalWriter == null) {
276 | return null; // closed
277 | }
278 | trimToSize();
279 | if (journalRebuildRequired()) {
280 | rebuildJournal();
281 | redundantOpCount = 0;
282 | }
283 | }
284 | return null;
285 | }
286 | };
287 |
288 | private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
289 | this.directory = directory;
290 | this.appVersion = appVersion;
291 | this.journalFile = new File(directory, JOURNAL_FILE);
292 | this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
293 | this.valueCount = valueCount;
294 | this.maxSize = maxSize;
295 | }
296 |
297 | /**
298 | * Opens the cache in {@code directory}, creating a cache if none exists
299 | * there.
300 | *
301 | * @param directory a writable directory
302 | * @param appVersion
303 | * @param valueCount the number of values per cache entry. Must be positive.
304 | * @param maxSize the maximum number of bytes this cache should use to store
305 | * @throws IOException if reading or writing the cache directory fails
306 | */
307 | public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
308 | throws IOException {
309 | if (maxSize <= 0) {
310 | throw new IllegalArgumentException("maxSize <= 0");
311 | }
312 | if (valueCount <= 0) {
313 | throw new IllegalArgumentException("valueCount <= 0");
314 | }
315 |
316 | // prefer to pick up where we left off
317 | DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
318 | if (cache.journalFile.exists()) {
319 | try {
320 | cache.readJournal();
321 | cache.processJournal();
322 | cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
323 | IO_BUFFER_SIZE);
324 | return cache;
325 | } catch (IOException journalIsCorrupt) {
326 | // System.logW("DiskLruCache " + directory + " is corrupt: "
327 | // + journalIsCorrupt.getMessage() + ", removing");
328 | cache.delete();
329 | }
330 | }
331 |
332 | // create a new empty cache
333 | directory.mkdirs();
334 | cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
335 | cache.rebuildJournal();
336 | return cache;
337 | }
338 |
339 | private void readJournal() throws IOException {
340 | InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
341 | try {
342 | String magic = readAsciiLine(in);
343 | String version = readAsciiLine(in);
344 | String appVersionString = readAsciiLine(in);
345 | String valueCountString = readAsciiLine(in);
346 | String blank = readAsciiLine(in);
347 | if (!MAGIC.equals(magic)
348 | || !VERSION_1.equals(version)
349 | || !Integer.toString(appVersion).equals(appVersionString)
350 | || !Integer.toString(valueCount).equals(valueCountString)
351 | || !"".equals(blank)) {
352 | throw new IOException("unexpected journal header: ["
353 | + magic + ", " + version + ", " + valueCountString + ", " + blank + "]");
354 | }
355 |
356 | while (true) {
357 | try {
358 | readJournalLine(readAsciiLine(in));
359 | } catch (EOFException endOfJournal) {
360 | break;
361 | }
362 | }
363 | } finally {
364 | closeQuietly(in);
365 | }
366 | }
367 |
368 | private void readJournalLine(String line) throws IOException {
369 | String[] parts = line.split(" ");
370 | if (parts.length < 2) {
371 | throw new IOException("unexpected journal line: " + line);
372 | }
373 |
374 | String key = parts[1];
375 | if (parts[0].equals(REMOVE) && parts.length == 2) {
376 | lruEntries.remove(key);
377 | return;
378 | }
379 |
380 | Entry entry = lruEntries.get(key);
381 | if (entry == null) {
382 | entry = new Entry(key);
383 | lruEntries.put(key, entry);
384 | }
385 |
386 | if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
387 | entry.readable = true;
388 | entry.currentEditor = null;
389 | entry.setLengths(copyOfRange(parts, 2, parts.length));
390 | } else if (parts[0].equals(DIRTY) && parts.length == 2) {
391 | entry.currentEditor = new Editor(entry);
392 | } else if (parts[0].equals(READ) && parts.length == 2) {
393 | // this work was already done by calling lruEntries.get()
394 | } else {
395 | throw new IOException("unexpected journal line: " + line);
396 | }
397 | }
398 |
399 | /**
400 | * Computes the initial size and collects garbage as a part of opening the
401 | * cache. Dirty entries are assumed to be inconsistent and will be deleted.
402 | */
403 | private void processJournal() throws IOException {
404 | deleteIfExists(journalFileTmp);
405 | for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) {
406 | Entry entry = i.next();
407 | if (entry.currentEditor == null) {
408 | for (int t = 0; t < valueCount; t++) {
409 | size += entry.lengths[t];
410 | }
411 | } else {
412 | entry.currentEditor = null;
413 | for (int t = 0; t < valueCount; t++) {
414 | deleteIfExists(entry.getCleanFile(t));
415 | deleteIfExists(entry.getDirtyFile(t));
416 | }
417 | i.remove();
418 | }
419 | }
420 | }
421 |
422 | /**
423 | * Creates a new journal that omits redundant information. This replaces the
424 | * current journal if it exists.
425 | */
426 | private synchronized void rebuildJournal() throws IOException {
427 | if (journalWriter != null) {
428 | journalWriter.close();
429 | }
430 |
431 | Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
432 | writer.write(MAGIC);
433 | writer.write("\n");
434 | writer.write(VERSION_1);
435 | writer.write("\n");
436 | writer.write(Integer.toString(appVersion));
437 | writer.write("\n");
438 | writer.write(Integer.toString(valueCount));
439 | writer.write("\n");
440 | writer.write("\n");
441 |
442 | for (Entry entry : lruEntries.values()) {
443 | if (entry.currentEditor != null) {
444 | writer.write(DIRTY + ' ' + entry.key + '\n');
445 | } else {
446 | writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
447 | }
448 | }
449 |
450 | writer.close();
451 | journalFileTmp.renameTo(journalFile);
452 | journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
453 | }
454 |
455 | private static void deleteIfExists(File file) throws IOException {
456 | // try {
457 | // Libcore.os.remove(file.getPath());
458 | // } catch (ErrnoException errnoException) {
459 | // if (errnoException.errno != OsConstants.ENOENT) {
460 | // throw errnoException.rethrowAsIOException();
461 | // }
462 | // }
463 | if (file.exists() && !file.delete()) {
464 | throw new IOException();
465 | }
466 | }
467 |
468 | /**
469 | * Returns a snapshot of the entry named {@code key}, or null if it doesn't
470 | * exist is not currently readable. If a value is returned, it is moved to
471 | * the head of the LRU queue.
472 | */
473 | public synchronized Snapshot get(String key) throws IOException {
474 | checkNotClosed();
475 | validateKey(key);
476 | Entry entry = lruEntries.get(key);
477 | if (entry == null) {
478 | return null;
479 | }
480 |
481 | if (!entry.readable) {
482 | return null;
483 | }
484 |
485 | /*
486 | * Open all streams eagerly to guarantee that we see a single published
487 | * snapshot. If we opened streams lazily then the streams could come
488 | * from different edits.
489 | */
490 | InputStream[] ins = new InputStream[valueCount];
491 | try {
492 | for (int i = 0; i < valueCount; i++) {
493 | ins[i] = new FileInputStream(entry.getCleanFile(i));
494 | }
495 | } catch (FileNotFoundException e) {
496 | // a file must have been deleted manually!
497 | return null;
498 | }
499 |
500 | redundantOpCount++;
501 | journalWriter.append(READ + ' ' + key + '\n');
502 | if (journalRebuildRequired()) {
503 | executorService.submit(cleanupCallable);
504 | }
505 |
506 | return new Snapshot(key, entry.sequenceNumber, ins);
507 | }
508 |
509 | /**
510 | * Returns an editor for the entry named {@code key}, or null if another
511 | * edit is in progress.
512 | */
513 | public Editor edit(String key) throws IOException {
514 | return edit(key, ANY_SEQUENCE_NUMBER);
515 | }
516 |
517 | private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
518 | checkNotClosed();
519 | validateKey(key);
520 | Entry entry = lruEntries.get(key);
521 | if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
522 | && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
523 | return null; // snapshot is stale
524 | }
525 | if (entry == null) {
526 | entry = new Entry(key);
527 | lruEntries.put(key, entry);
528 | } else if (entry.currentEditor != null) {
529 | return null; // another edit is in progress
530 | }
531 |
532 | Editor editor = new Editor(entry);
533 | entry.currentEditor = editor;
534 |
535 | // flush the journal before creating files to prevent file leaks
536 | journalWriter.write(DIRTY + ' ' + key + '\n');
537 | journalWriter.flush();
538 | return editor;
539 | }
540 |
541 | /**
542 | * Returns the directory where this cache stores its data.
543 | */
544 | public File getDirectory() {
545 | return directory;
546 | }
547 |
548 | /**
549 | * Returns the maximum number of bytes that this cache should use to store
550 | * its data.
551 | */
552 | public long maxSize() {
553 | return maxSize;
554 | }
555 |
556 | /**
557 | * Returns the number of bytes currently being used to store the values in
558 | * this cache. This may be greater than the max size if a background
559 | * deletion is pending.
560 | */
561 | public synchronized long size() {
562 | return size;
563 | }
564 |
565 | private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
566 | Entry entry = editor.entry;
567 | if (entry.currentEditor != editor) {
568 | throw new IllegalStateException();
569 | }
570 |
571 | // if this edit is creating the entry for the first time, every index must have a value
572 | if (success && !entry.readable) {
573 | for (int i = 0; i < valueCount; i++) {
574 | if (!entry.getDirtyFile(i).exists()) {
575 | editor.abort();
576 | throw new IllegalStateException("edit didn't create file " + i);
577 | }
578 | }
579 | }
580 |
581 | for (int i = 0; i < valueCount; i++) {
582 | File dirty = entry.getDirtyFile(i);
583 | if (success) {
584 | if (dirty.exists()) {
585 | File clean = entry.getCleanFile(i);
586 | dirty.renameTo(clean);
587 | long oldLength = entry.lengths[i];
588 | long newLength = clean.length();
589 | entry.lengths[i] = newLength;
590 | size = size - oldLength + newLength;
591 | }
592 | } else {
593 | deleteIfExists(dirty);
594 | }
595 | }
596 |
597 | redundantOpCount++;
598 | entry.currentEditor = null;
599 | if (entry.readable | success) {
600 | entry.readable = true;
601 | journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
602 | if (success) {
603 | entry.sequenceNumber = nextSequenceNumber++;
604 | }
605 | } else {
606 | lruEntries.remove(entry.key);
607 | journalWriter.write(REMOVE + ' ' + entry.key + '\n');
608 | }
609 |
610 | if (size > maxSize || journalRebuildRequired()) {
611 | executorService.submit(cleanupCallable);
612 | }
613 | }
614 |
615 | /**
616 | * We only rebuild the journal when it will halve the size of the journal
617 | * and eliminate at least 2000 ops.
618 | */
619 | private boolean journalRebuildRequired() {
620 | final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
621 | return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
622 | && redundantOpCount >= lruEntries.size();
623 | }
624 |
625 | /**
626 | * Drops the entry for {@code key} if it exists and can be removed. Entries
627 | * actively being edited cannot be removed.
628 | *
629 | * @return true if an entry was removed.
630 | */
631 | public synchronized boolean remove(String key) throws IOException {
632 | checkNotClosed();
633 | validateKey(key);
634 | Entry entry = lruEntries.get(key);
635 | if (entry == null || entry.currentEditor != null) {
636 | return false;
637 | }
638 |
639 | for (int i = 0; i < valueCount; i++) {
640 | File file = entry.getCleanFile(i);
641 | if (!file.delete()) {
642 | throw new IOException("failed to delete " + file);
643 | }
644 | size -= entry.lengths[i];
645 | entry.lengths[i] = 0;
646 | }
647 |
648 | redundantOpCount++;
649 | journalWriter.append(REMOVE + ' ' + key + '\n');
650 | lruEntries.remove(key);
651 |
652 | if (journalRebuildRequired()) {
653 | executorService.submit(cleanupCallable);
654 | }
655 |
656 | return true;
657 | }
658 |
659 | /**
660 | * Returns true if this cache has been closed.
661 | */
662 | public boolean isClosed() {
663 | return journalWriter == null;
664 | }
665 |
666 | private void checkNotClosed() {
667 | if (journalWriter == null) {
668 | throw new IllegalStateException("cache is closed");
669 | }
670 | }
671 |
672 | /**
673 | * Force buffered operations to the filesystem.
674 | */
675 | public synchronized void flush() throws IOException {
676 | checkNotClosed();
677 | trimToSize();
678 | journalWriter.flush();
679 | }
680 |
681 | /**
682 | * Closes this cache. Stored values will remain on the filesystem.
683 | */
684 | public synchronized void close() throws IOException {
685 | if (journalWriter == null) {
686 | return; // already closed
687 | }
688 | for (Entry entry : new ArrayList(lruEntries.values())) {
689 | if (entry.currentEditor != null) {
690 | entry.currentEditor.abort();
691 | }
692 | }
693 | trimToSize();
694 | journalWriter.close();
695 | journalWriter = null;
696 | }
697 |
698 | private void trimToSize() throws IOException {
699 | while (size > maxSize) {
700 | // Map.Entry toEvict = lruEntries.eldest();
701 | final Map.Entry toEvict = lruEntries.entrySet().iterator().next();
702 | remove(toEvict.getKey());
703 | }
704 | }
705 |
706 | /**
707 | * Closes the cache and deletes all of its stored values. This will delete
708 | * all files in the cache directory including files that weren't created by
709 | * the cache.
710 | */
711 | public void delete() throws IOException {
712 | close();
713 | deleteContents(directory);
714 | }
715 |
716 | private void validateKey(String key) {
717 | if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
718 | throw new IllegalArgumentException(
719 | "keys must not contain spaces or newlines: \"" + key + "\"");
720 | }
721 | }
722 |
723 | private static String inputStreamToString(InputStream in) throws IOException {
724 | return readFully(new InputStreamReader(in, UTF_8));
725 | }
726 |
727 | /**
728 | * A snapshot of the values for an entry.
729 | */
730 | public final class Snapshot implements Closeable {
731 | private final String key;
732 | private final long sequenceNumber;
733 | private final InputStream[] ins;
734 |
735 | private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
736 | this.key = key;
737 | this.sequenceNumber = sequenceNumber;
738 | this.ins = ins;
739 | }
740 |
741 | /**
742 | * Returns an editor for this snapshot's entry, or null if either the
743 | * entry has changed since this snapshot was created or if another edit
744 | * is in progress.
745 | */
746 | public Editor edit() throws IOException {
747 | return DiskLruCache.this.edit(key, sequenceNumber);
748 | }
749 |
750 | /**
751 | * Returns the unbuffered stream with the value for {@code index}.
752 | */
753 | public InputStream getInputStream(int index) {
754 | return ins[index];
755 | }
756 |
757 | /**
758 | * Returns the string value for {@code index}.
759 | */
760 | public String getString(int index) throws IOException {
761 | return inputStreamToString(getInputStream(index));
762 | }
763 |
764 | @Override public void close() {
765 | for (InputStream in : ins) {
766 | closeQuietly(in);
767 | }
768 | }
769 | }
770 |
771 | /**
772 | * Edits the values for an entry.
773 | */
774 | public final class Editor {
775 | private final Entry entry;
776 | private boolean hasErrors;
777 |
778 | private Editor(Entry entry) {
779 | this.entry = entry;
780 | }
781 |
782 | /**
783 | * Returns an unbuffered input stream to read the last committed value,
784 | * or null if no value has been committed.
785 | */
786 | public InputStream newInputStream(int index) throws IOException {
787 | synchronized (DiskLruCache.this) {
788 | if (entry.currentEditor != this) {
789 | throw new IllegalStateException();
790 | }
791 | if (!entry.readable) {
792 | return null;
793 | }
794 | return new FileInputStream(entry.getCleanFile(index));
795 | }
796 | }
797 |
798 | /**
799 | * Returns the last committed value as a string, or null if no value
800 | * has been committed.
801 | */
802 | public String getString(int index) throws IOException {
803 | InputStream in = newInputStream(index);
804 | return in != null ? inputStreamToString(in) : null;
805 | }
806 |
807 | /**
808 | * Returns a new unbuffered output stream to write the value at
809 | * {@code index}. If the underlying output stream encounters errors
810 | * when writing to the filesystem, this edit will be aborted when
811 | * {@link #commit} is called. The returned output stream does not throw
812 | * IOExceptions.
813 | */
814 | public OutputStream newOutputStream(int index) throws IOException {
815 | synchronized (DiskLruCache.this) {
816 | if (entry.currentEditor != this) {
817 | throw new IllegalStateException();
818 | }
819 | return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
820 | }
821 | }
822 |
823 | /**
824 | * Sets the value at {@code index} to {@code value}.
825 | */
826 | public void set(int index, String value) throws IOException {
827 | Writer writer = null;
828 | try {
829 | writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
830 | writer.write(value);
831 | } finally {
832 | closeQuietly(writer);
833 | }
834 | }
835 |
836 | /**
837 | * Commits this edit so it is visible to readers. This releases the
838 | * edit lock so another edit may be started on the same key.
839 | */
840 | public void commit() throws IOException {
841 | if (hasErrors) {
842 | completeEdit(this, false);
843 | remove(entry.key); // the previous entry is stale
844 | } else {
845 | completeEdit(this, true);
846 | }
847 | }
848 |
849 | /**
850 | * Aborts this edit. This releases the edit lock so another edit may be
851 | * started on the same key.
852 | */
853 | public void abort() throws IOException {
854 | completeEdit(this, false);
855 | }
856 |
857 | private class FaultHidingOutputStream extends FilterOutputStream {
858 | private FaultHidingOutputStream(OutputStream out) {
859 | super(out);
860 | }
861 |
862 | @Override public void write(int oneByte) {
863 | try {
864 | out.write(oneByte);
865 | } catch (IOException e) {
866 | hasErrors = true;
867 | }
868 | }
869 |
870 | @Override public void write(byte[] buffer, int offset, int length) {
871 | try {
872 | out.write(buffer, offset, length);
873 | } catch (IOException e) {
874 | hasErrors = true;
875 | }
876 | }
877 |
878 | @Override public void close() {
879 | try {
880 | out.close();
881 | } catch (IOException e) {
882 | hasErrors = true;
883 | }
884 | }
885 |
886 | @Override public void flush() {
887 | try {
888 | out.flush();
889 | } catch (IOException e) {
890 | hasErrors = true;
891 | }
892 | }
893 | }
894 | }
895 |
896 | private final class Entry {
897 | private final String key;
898 |
899 | /** Lengths of this entry's files. */
900 | private final long[] lengths;
901 |
902 | /** True if this entry has ever been published */
903 | private boolean readable;
904 |
905 | /** The ongoing edit or null if this entry is not being edited. */
906 | private Editor currentEditor;
907 |
908 | /** The sequence number of the most recently committed edit to this entry. */
909 | private long sequenceNumber;
910 |
911 | private Entry(String key) {
912 | this.key = key;
913 | this.lengths = new long[valueCount];
914 | }
915 |
916 | public String getLengths() throws IOException {
917 | StringBuilder result = new StringBuilder();
918 | for (long size : lengths) {
919 | result.append(' ').append(size);
920 | }
921 | return result.toString();
922 | }
923 |
924 | /**
925 | * Set lengths using decimal numbers like "10123".
926 | */
927 | private void setLengths(String[] strings) throws IOException {
928 | if (strings.length != valueCount) {
929 | throw invalidLengths(strings);
930 | }
931 |
932 | try {
933 | for (int i = 0; i < strings.length; i++) {
934 | lengths[i] = Long.parseLong(strings[i]);
935 | }
936 | } catch (NumberFormatException e) {
937 | throw invalidLengths(strings);
938 | }
939 | }
940 |
941 | private IOException invalidLengths(String[] strings) throws IOException {
942 | throw new IOException("unexpected journal line: " + Arrays.toString(strings));
943 | }
944 |
945 | public File getCleanFile(int i) {
946 | return new File(directory, key + "." + i);
947 | }
948 |
949 | public File getDirtyFile(int i) {
950 | return new File(directory, key + "." + i + ".tmp");
951 | }
952 | }
953 | }
954 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xc/xccache/MemoryCache.java:
--------------------------------------------------------------------------------
1 | package com.xc.xccache;
2 |
3 | import android.support.v4.util.LruCache;
4 |
5 | /**
6 | * 内存缓存类
7 | * Created by caizhiming on 2015/12/4.
8 | */
9 | public class MemoryCache implements Cache {
10 | private LruCache mMemoryLruCache;
11 | private EvictedListener mEvictedListener;
12 |
13 | public MemoryCache() {
14 | init();
15 | }
16 |
17 | public MemoryCache(EvictedListener listener) {
18 | init();
19 | this.mEvictedListener = listener;
20 | }
21 |
22 | public void setEvictedListener(EvictedListener listener) {
23 | this.mEvictedListener = listener;
24 | }
25 |
26 | public boolean hasEvictedListener() {
27 | return mEvictedListener != null;
28 | }
29 |
30 | private void init() {
31 | // 计算可使用的最大内存
32 | final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
33 | // 取可用内存空间的1/4作为缓存
34 | final int cacheSize = maxMemory / 4;
35 | mMemoryLruCache = new LruCache(cacheSize) {
36 | @Override
37 | protected int sizeOf(String key, String value) {
38 | return value.getBytes().length;
39 | }
40 |
41 | @Override
42 | protected void entryRemoved(boolean evicted, String key, String oldValue, String newValue) {
43 | if (evicted) {
44 | if (mEvictedListener != null) {
45 | mEvictedListener.handleEvictEntry(key, oldValue);
46 | }
47 | }
48 | }
49 | };
50 | }
51 |
52 | @Override
53 | public String get(String key) {
54 | return mMemoryLruCache.get(key);
55 | }
56 |
57 | @Override
58 | public void put(String key, String value) {
59 | mMemoryLruCache.put(key, value);
60 | }
61 |
62 | @Override
63 | public boolean remove(String key) {
64 | return Boolean.parseBoolean(mMemoryLruCache.remove(key));
65 | }
66 |
67 | /**
68 | * called when mMemoryLruCache evict entrys,
69 | *
70 | * using by CacheManager.Strategy.MEMORY_FIRST
71 | */
72 | public interface EvictedListener {
73 | void handleEvictEntry(String evictKey, String evictValue);
74 | }
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xc/xccache/XCCacheManager.java:
--------------------------------------------------------------------------------
1 | package com.xc.xccache;
2 |
3 | import android.content.Context;
4 |
5 | import java.util.concurrent.Callable;
6 | import java.util.concurrent.ExecutionException;
7 | import java.util.concurrent.ExecutorService;
8 | import java.util.concurrent.Executors;
9 | import java.util.concurrent.Future;
10 |
11 | /**
12 | * Created by caizhiming on 2015/11/27.
13 | * 使用内存缓存和Disk缓存双缓存的Http缓存管理类
14 | */
15 | public class XCCacheManager {
16 |
17 | private static XCCacheManager mInstance = null;
18 |
19 | private Strategy mStrategy = Strategy.MEMORY_FIRST;
20 | //线程池
21 | private ExecutorService mExecutor = null;
22 | //内存缓存
23 | private MemoryCache mMemoryCache;
24 | //Disk缓存
25 | private DiskCache mDiskCache;
26 |
27 | public static XCCacheManager getInstance(Context context) {
28 | return getInstance(context, Strategy.MEMORY_FIRST);
29 | }
30 |
31 | public static XCCacheManager getInstance(Context context, Strategy strategy) {
32 | if (mInstance == null) {
33 | synchronized (XCCacheManager.class) {
34 | if (mInstance == null) {
35 | mInstance = new XCCacheManager(context.getApplicationContext(), strategy);
36 | }
37 | }
38 | } else {
39 | mInstance.setStrategy(strategy);
40 | }
41 | return mInstance;
42 | }
43 |
44 | private XCCacheManager(Context context, Strategy strategy) {
45 | this.mStrategy = strategy;
46 | init(context);
47 | }
48 |
49 | public void setStrategy(XCCacheManager.Strategy strategy) {
50 | this.mStrategy = strategy;
51 | switch (mStrategy) {
52 | case MEMORY_FIRST:
53 | if (!mMemoryCache.hasEvictedListener()) {
54 | mMemoryCache.setEvictedListener(new MemoryCache.EvictedListener() {
55 | @Override
56 | public void handleEvictEntry(String evictKey, String evictValue) {
57 | mDiskCache.put(evictKey, evictValue);
58 | }
59 | });
60 | }
61 | break;
62 | case MEMORY_ONLY:
63 | if (mMemoryCache.hasEvictedListener())
64 | mMemoryCache.setEvictedListener(null);
65 | break;
66 | case DISK_ONLY:
67 | break;
68 | }
69 | }
70 |
71 | public String getCurStrategy() {
72 | return mStrategy.name();
73 | }
74 |
75 | /**
76 | * 初始化 DiskLruCache
77 | */
78 | private void init(Context context) {
79 | mExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
80 | mDiskCache = new DiskCache(context);
81 | mMemoryCache = new MemoryCache();
82 | }
83 |
84 | /**
85 | * 从缓存中读取value
86 | */
87 | public String readCache(final String key) {
88 | Future ret = mExecutor.submit(new Callable() {
89 | @Override
90 | public String call() throws Exception {
91 | String result = null;
92 | switch (mStrategy) {
93 | case MEMORY_ONLY:
94 | result = mMemoryCache.get(key);
95 | break;
96 | case MEMORY_FIRST:
97 | result = mMemoryCache.get(key);
98 | if (result == null) {
99 | result = mDiskCache.get(key);
100 | }
101 | break;
102 | case DISK_ONLY:
103 | result = mDiskCache.get(key);
104 | break;
105 | }
106 | return result;
107 | }
108 | });
109 | try {
110 | return ret.get();
111 | } catch (InterruptedException e) {
112 | e.printStackTrace();
113 | } catch (ExecutionException e) {
114 | e.printStackTrace();
115 | }
116 | return null;
117 | }
118 |
119 | /**
120 | * 将value 写入到缓存中
121 | */
122 | public void writeCache(final String key, final String value) {
123 | mExecutor.submit(new Runnable() {
124 | @Override
125 | public void run() {
126 | switch (mStrategy) {
127 | case MEMORY_FIRST:
128 | mMemoryCache.put(key, value);
129 | mDiskCache.put(key,value);
130 | break;
131 | case MEMORY_ONLY:
132 | mMemoryCache.put(key, value);
133 | break;
134 | case DISK_ONLY:
135 | mDiskCache.put(key, value);
136 | break;
137 | }
138 | }
139 | });
140 | }
141 |
142 | enum Strategy {
143 | MEMORY_ONLY(0), MEMORY_FIRST(1), DISK_ONLY(3);
144 | int id;
145 |
146 | Strategy(int id) {
147 | this.id = id;
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/app/src/main/java/com/xc/xccachemanager/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.xc.xccachemanager;
2 |
3 | import android.app.Activity;
4 | import android.os.Bundle;
5 | import android.view.View;
6 | import android.widget.Button;
7 | import android.widget.EditText;
8 | import android.widget.TextView;
9 | import android.widget.Toast;
10 |
11 | import com.xc.xccache.XCCacheManager;
12 |
13 | public class MainActivity extends Activity implements View.OnClickListener {
14 | private Button mBtnWrite;
15 | private Button mBtnRead;
16 | private EditText mEtText;
17 | private TextView mTvResult;
18 |
19 | private XCCacheManager mCacheManager;
20 |
21 | @Override
22 | protected void onCreate(Bundle savedInstanceState) {
23 | super.onCreate(savedInstanceState);
24 | setContentView(R.layout.activity_main);
25 | mBtnRead = (Button) findViewById(R.id.btn_read);
26 | mBtnWrite = (Button) findViewById(R.id.btn_write);
27 | mEtText = (EditText) findViewById(R.id.et_text);
28 | mTvResult = (TextView) findViewById(R.id.tv_result);
29 | mBtnWrite.setOnClickListener(this);
30 | mBtnRead.setOnClickListener(this);
31 |
32 | mCacheManager = XCCacheManager.getInstance(this);
33 | }
34 |
35 | @Override
36 | public void onClick(View v) {
37 | switch (v.getId()){
38 | case R.id.btn_read:
39 | mTvResult.setText(mCacheManager.readCache("key_demo"));
40 | break;
41 | case R.id.btn_write:
42 | mCacheManager.writeCache("key_demo",mEtText.getText().toString());
43 | Toast.makeText(this,"write string to cache",Toast.LENGTH_SHORT).show();
44 | break;
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
15 |
20 |
25 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jczmdeveloper/XCCacheManager/0c2e86606192b91d8ae4889a9dd15adfad70a27b/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jczmdeveloper/XCCacheManager/0c2e86606192b91d8ae4889a9dd15adfad70a27b/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jczmdeveloper/XCCacheManager/0c2e86606192b91d8ae4889a9dd15adfad70a27b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jczmdeveloper/XCCacheManager/0c2e86606192b91d8ae4889a9dd15adfad70a27b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jczmdeveloper/XCCacheManager/0c2e86606192b91d8ae4889a9dd15adfad70a27b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3F51B5
4 | #303F9F
5 | #FF4081
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 16dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | XCCacheManager
3 | Settings
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | repositories {
5 | jcenter()
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:1.5.0'
9 |
10 | // NOTE: Do not place your application dependencies here; they belong
11 | // in the individual module build.gradle files
12 | }
13 | }
14 |
15 | allprojects {
16 | repositories {
17 | jcenter()
18 | }
19 | }
20 |
21 | task clean(type: Delete) {
22 | delete rootProject.buildDir
23 | }
24 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jczmdeveloper/XCCacheManager/0c2e86606192b91d8ae4889a9dd15adfad70a27b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Oct 21 11:34:03 PDT 2015
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/screenshots/01.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jczmdeveloper/XCCacheManager/0c2e86606192b91d8ae4889a9dd15adfad70a27b/screenshots/01.gif
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------