Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
14792 manas 1
/**
2
 * Copyright 2010-present Facebook.
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *    http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
 
17
package com.facebook.internal;
18
 
19
import android.content.Context;
20
import android.util.Log;
21
import com.facebook.LoggingBehavior;
22
import com.facebook.Settings;
23
import org.json.JSONException;
24
import org.json.JSONObject;
25
import org.json.JSONTokener;
26
 
27
import java.io.*;
28
import java.security.InvalidParameterException;
29
import java.util.Date;
30
import java.util.PriorityQueue;
31
import java.util.concurrent.atomic.AtomicLong;
32
 
33
// This class is intended to be thread-safe.
34
//
35
// There are two classes of files:  buffer files and cache files:
36
// - A buffer file is in the process of being written, and there is an open stream on the file.  These files are
37
//   named as "bufferN" where N is an incrementing integer.  On startup, we delete all existing files of this form.
38
//   Once the stream is closed, we rename the buffer file to a cache file or attempt to delete if this fails.  We
39
//   do not otherwise ever attempt to delete these files.
40
// - A cache file is a non-changing file that is named by the md5 hash of the cache key.  We monitor the size of
41
//   these files in aggregate and remove the oldest one(s) to stay under quota.  This process does not block threads
42
//   calling into this class, so theoretically we could go arbitrarily over quota but in practice this should not
43
//   happen because deleting files should be much cheaper than downloading new file content.
44
//
45
// Since there can only ever be one thread accessing a particular buffer file, we do not synchronize access to these.
46
// We do assume that file rename is atomic when converting a buffer file to a cache file, and that if multiple files
47
// are renamed to a single target that exactly one of them continues to exist.
48
//
49
// Standard POSIX file semantics guarantee being able to continue to use a file handle even after the
50
// corresponding file has been deleted.  Given this and that cache files never change other than deleting in trim()
51
// or clear(),  we only have to ensure that there is at most one trim() or clear() process deleting files at any
52
// given time.
53
 
54
/**
55
 * com.facebook.internal is solely for the use of other packages within the Facebook SDK for Android. Use of
56
 * any of the classes in this package is unsupported, and they may be modified or removed without warning at
57
 * any time.
58
 */
59
public final class FileLruCache {
60
    static final String TAG = FileLruCache.class.getSimpleName();
61
    private static final String HEADER_CACHEKEY_KEY = "key";
62
    private static final String HEADER_CACHE_CONTENT_TAG_KEY = "tag";
63
 
64
    private static final AtomicLong bufferIndex = new AtomicLong();
65
 
66
    private final String tag;
67
    private final Limits limits;
68
    private final File directory;
69
    private boolean isTrimPending;
70
    private boolean isTrimInProgress;
71
    private final Object lock;
72
    private AtomicLong lastClearCacheTime = new AtomicLong(0);
73
 
74
    // The value of tag should be a final String that works as a directory name.
75
    public FileLruCache(Context context, String tag, Limits limits) {
76
        this.tag = tag;
77
        this.limits = limits;
78
        this.directory = new File(context.getCacheDir(), tag);
79
        this.lock = new Object();
80
 
81
        // Ensure the cache dir exists
82
        if (this.directory.mkdirs() || this.directory.isDirectory()) {
83
            // Remove any stale partially-written files from a previous run
84
            BufferFile.deleteAll(this.directory);
85
        }
86
    }
87
 
88
    // This is not robust to files changing dynamically underneath it and should therefore only be used
89
    // for test code.  If we ever need this for product code we need to think through synchronization.
90
    // See the threading notes at the top of this class.
91
    //
92
    // Also, since trim() runs asynchronously now, this blocks until any pending trim has completed.
93
    long sizeInBytesForTest() {
94
        synchronized (lock) {
95
            while (isTrimPending || isTrimInProgress) {
96
                try {
97
                    lock.wait();
98
                } catch (InterruptedException e) {
99
                    // intentional no-op
100
                }
101
            }
102
        }
103
 
104
        File[] files = this.directory.listFiles();
105
        long total = 0;
106
        if (files != null) {
107
            for (File file : files) {
108
                total += file.length();
109
            }
110
        }
111
        return total;
112
    }
113
 
114
    public InputStream get(String key) throws IOException {
115
        return get(key, null);
116
    }
117
 
118
    public InputStream get(String key, String contentTag) throws IOException {
119
        File file = new File(this.directory, Utility.md5hash(key));
120
 
121
        FileInputStream input = null;
122
        try {
123
            input = new FileInputStream(file);
124
        } catch (IOException e) {
125
            return null;
126
        }
127
 
128
        BufferedInputStream buffered = new BufferedInputStream(input, Utility.DEFAULT_STREAM_BUFFER_SIZE);
129
        boolean success = false;
130
 
131
        try {
132
            JSONObject header = StreamHeader.readHeader(buffered);
133
            if (header == null) {
134
                return null;
135
            }
136
 
137
            String foundKey = header.optString(HEADER_CACHEKEY_KEY);
138
            if ((foundKey == null) || !foundKey.equals(key)) {
139
                return null;
140
            }
141
 
142
            String headerContentTag = header.optString(HEADER_CACHE_CONTENT_TAG_KEY, null);
143
 
144
            if ((contentTag == null && headerContentTag != null) ||
145
                    (contentTag != null && !contentTag.equals(headerContentTag))) {
146
                return null;
147
            }
148
 
149
            long accessTime = new Date().getTime();
150
            Logger.log(LoggingBehavior.CACHE, TAG, "Setting lastModified to " + Long.valueOf(accessTime) + " for "
151
                    + file.getName());
152
            file.setLastModified(accessTime);
153
 
154
            success = true;
155
            return buffered;
156
        } finally {
157
            if (!success) {
158
                buffered.close();
159
            }
160
        }
161
    }
162
 
163
    OutputStream openPutStream(final String key) throws IOException {
164
        return openPutStream(key, null);
165
    }
166
 
167
    public OutputStream openPutStream(final String key, String contentTag) throws IOException {
168
        final File buffer = BufferFile.newFile(this.directory);
169
        buffer.delete();
170
        if (!buffer.createNewFile()) {
171
            throw new IOException("Could not create file at " + buffer.getAbsolutePath());
172
        }
173
 
174
        FileOutputStream file = null;
175
        try {
176
            file = new FileOutputStream(buffer);
177
        } catch (FileNotFoundException e) {
178
            Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "Error creating buffer output stream: " + e);
179
            throw new IOException(e.getMessage());
180
        }
181
 
182
        final long bufferFileCreateTime = System.currentTimeMillis();
183
        StreamCloseCallback renameToTargetCallback = new StreamCloseCallback() {
184
            @Override
185
            public void onClose() {
186
                // if the buffer file was created before the cache was cleared, then the buffer file
187
                // should be deleted rather than renamed and saved.
188
                if (bufferFileCreateTime < lastClearCacheTime.get()) {
189
                    buffer.delete();
190
                } else {
191
                    renameToTargetAndTrim(key, buffer);
192
                }
193
            }
194
        };
195
 
196
        CloseCallbackOutputStream cleanup = new CloseCallbackOutputStream(file, renameToTargetCallback);
197
        BufferedOutputStream buffered = new BufferedOutputStream(cleanup, Utility.DEFAULT_STREAM_BUFFER_SIZE);
198
        boolean success = false;
199
 
200
        try {
201
            // Prefix the stream with the actual key, since there could be collisions
202
            JSONObject header = new JSONObject();
203
            header.put(HEADER_CACHEKEY_KEY, key);
204
            if (!Utility.isNullOrEmpty(contentTag)) {
205
                header.put(HEADER_CACHE_CONTENT_TAG_KEY, contentTag);
206
            }
207
 
208
            StreamHeader.writeHeader(buffered, header);
209
 
210
            success = true;
211
            return buffered;
212
        } catch (JSONException e) {
213
            // JSON is an implementation detail of the cache, so don't let JSON exceptions out.
214
            Logger.log(LoggingBehavior.CACHE, Log.WARN, TAG, "Error creating JSON header for cache file: " + e);
215
            throw new IOException(e.getMessage());
216
        } finally {
217
            if (!success) {
218
                buffered.close();
219
            }
220
        }
221
    }
222
 
223
    public void clearCache() {
224
        // get the current directory listing of files to delete
225
        final File[] filesToDelete = directory.listFiles(BufferFile.excludeBufferFiles());
226
        lastClearCacheTime.set(System.currentTimeMillis());
227
        if (filesToDelete != null) {
228
            Settings.getExecutor().execute(new Runnable() {
229
                @Override
230
                public void run() {
231
                    for (File file : filesToDelete) {
232
                        file.delete();
233
                    }
234
                }
235
            });
236
        }
237
    }
238
 
239
    private void renameToTargetAndTrim(String key, File buffer) {
240
        final File target = new File(directory, Utility.md5hash(key));
241
 
242
        // This is triggered by close().  By the time close() returns, the file should be cached, so this needs to
243
        // happen synchronously on this thread.
244
        //
245
        // However, it does not need to be synchronized, since in the race we will just start an unnecesary trim
246
        // operation.  Avoiding the cost of holding the lock across the file operation seems worth this cost.
247
        if (!buffer.renameTo(target)) {
248
            buffer.delete();
249
        }
250
 
251
        postTrim();
252
    }
253
 
254
    // Opens an output stream for the key, and creates an input stream wrapper to copy
255
    // the contents of input into the new output stream.  The effect is to store a
256
    // copy of input, and associate that data with key.
257
    public InputStream interceptAndPut(String key, InputStream input) throws IOException {
258
        OutputStream output = openPutStream(key);
259
        return new CopyingInputStream(input, output);
260
    }
261
 
262
    public String toString() {
263
        return "{FileLruCache:" + " tag:" + this.tag + " file:" + this.directory.getName() + "}";
264
    }
265
 
266
    private void postTrim() {
267
        synchronized (lock) {
268
            if (!isTrimPending) {
269
                isTrimPending = true;
270
                Settings.getExecutor().execute(new Runnable() {
271
                    @Override
272
                    public void run() {
273
                        trim();
274
                    }
275
                });
276
            }
277
        }
278
    }
279
 
280
    private void trim() {
281
        synchronized (lock) {
282
            isTrimPending = false;
283
            isTrimInProgress = true;
284
        }
285
        try {
286
            Logger.log(LoggingBehavior.CACHE, TAG, "trim started");
287
            PriorityQueue<ModifiedFile> heap = new PriorityQueue<ModifiedFile>();
288
            long size = 0;
289
            long count = 0;
290
            File[] filesToTrim =this.directory.listFiles(BufferFile.excludeBufferFiles());
291
            if (filesToTrim != null) {
292
                for (File file : filesToTrim) {
293
                    ModifiedFile modified = new ModifiedFile(file);
294
                    heap.add(modified);
295
                    Logger.log(LoggingBehavior.CACHE, TAG, "  trim considering time=" + Long.valueOf(modified.getModified())
296
                            + " name=" + modified.getFile().getName());
297
 
298
                    size += file.length();
299
                    count++;
300
                }
301
            }
302
 
303
            while ((size > limits.getByteCount()) || (count > limits.getFileCount())) {
304
                File file = heap.remove().getFile();
305
                Logger.log(LoggingBehavior.CACHE, TAG, "  trim removing " + file.getName());
306
                size -= file.length();
307
                count--;
308
                file.delete();
309
            }
310
        } finally {
311
            synchronized (lock) {
312
                isTrimInProgress = false;
313
                lock.notifyAll();
314
            }
315
        }
316
    }
317
 
318
    private static class BufferFile {
319
        private static final String FILE_NAME_PREFIX = "buffer";
320
        private static final FilenameFilter filterExcludeBufferFiles = new FilenameFilter() {
321
            @Override
322
            public boolean accept(File dir, String filename) {
323
                return !filename.startsWith(FILE_NAME_PREFIX);
324
            }
325
        };
326
        private static final FilenameFilter filterExcludeNonBufferFiles = new FilenameFilter() {
327
            @Override
328
            public boolean accept(File dir, String filename) {
329
                return filename.startsWith(FILE_NAME_PREFIX);
330
            }
331
        };
332
 
333
        static void deleteAll(final File root) {
334
            File[] filesToDelete = root.listFiles(excludeNonBufferFiles());
335
            if (filesToDelete != null) {
336
                for (File file : filesToDelete) {
337
                    file.delete();
338
                }
339
            }
340
        }
341
 
342
        static FilenameFilter excludeBufferFiles() {
343
            return filterExcludeBufferFiles;
344
        }
345
 
346
        static FilenameFilter excludeNonBufferFiles() {
347
            return filterExcludeNonBufferFiles;
348
        }
349
 
350
        static File newFile(final File root) {
351
            String name = FILE_NAME_PREFIX + Long.valueOf(bufferIndex.incrementAndGet()).toString();
352
            return new File(root, name);
353
        }
354
    }
355
 
356
    // Treats the first part of a stream as a header, reads/writes it as a JSON blob, and
357
    // leaves the stream positioned exactly after the header.
358
    //
359
    // The format is as follows:
360
    //     byte: meaning
361
    // ---------------------------------
362
    //        0: version number
363
    //      1-3: big-endian JSON header blob size
364
    // 4-size+4: UTF-8 JSON header blob
365
    //      ...: stream data
366
    private static final class StreamHeader {
367
        private static final int HEADER_VERSION = 0;
368
 
369
        static void writeHeader(OutputStream stream, JSONObject header) throws IOException {
370
            String headerString = header.toString();
371
            byte[] headerBytes = headerString.getBytes();
372
 
373
            // Write version number and big-endian header size
374
            stream.write(HEADER_VERSION);
375
            stream.write((headerBytes.length >> 16) & 0xff);
376
            stream.write((headerBytes.length >> 8) & 0xff);
377
            stream.write((headerBytes.length >> 0) & 0xff);
378
 
379
            stream.write(headerBytes);
380
        }
381
 
382
        static JSONObject readHeader(InputStream stream) throws IOException {
383
            int version = stream.read();
384
            if (version != HEADER_VERSION) {
385
                return null;
386
            }
387
 
388
            int headerSize = 0;
389
            for (int i = 0; i < 3; i++) {
390
                int b = stream.read();
391
                if (b == -1) {
392
                    Logger.log(LoggingBehavior.CACHE, TAG,
393
                            "readHeader: stream.read returned -1 while reading header size");
394
                    return null;
395
                }
396
                headerSize <<= 8;
397
                headerSize += b & 0xff;
398
            }
399
 
400
            byte[] headerBytes = new byte[headerSize];
401
            int count = 0;
402
            while (count < headerBytes.length) {
403
                int readCount = stream.read(headerBytes, count, headerBytes.length - count);
404
                if (readCount < 1) {
405
                    Logger.log(LoggingBehavior.CACHE, TAG,
406
                            "readHeader: stream.read stopped at " + Integer.valueOf(count) + " when expected "
407
                                    + headerBytes.length);
408
                    return null;
409
                }
410
                count += readCount;
411
            }
412
 
413
            String headerString = new String(headerBytes);
414
            JSONObject header = null;
415
            JSONTokener tokener = new JSONTokener(headerString);
416
            try {
417
                Object parsed = tokener.nextValue();
418
                if (!(parsed instanceof JSONObject)) {
419
                    Logger.log(LoggingBehavior.CACHE, TAG, "readHeader: expected JSONObject, got " + parsed.getClass().getCanonicalName());
420
                    return null;
421
                }
422
                header = (JSONObject) parsed;
423
            } catch (JSONException e) {
424
                throw new IOException(e.getMessage());
425
            }
426
 
427
            return header;
428
        }
429
    }
430
 
431
    private static class CloseCallbackOutputStream extends OutputStream {
432
        final OutputStream innerStream;
433
        final StreamCloseCallback callback;
434
 
435
        CloseCallbackOutputStream(OutputStream innerStream, StreamCloseCallback callback) {
436
            this.innerStream = innerStream;
437
            this.callback = callback;
438
        }
439
 
440
        @Override
441
        public void close() throws IOException {
442
            try {
443
                this.innerStream.close();
444
            } finally {
445
                this.callback.onClose();
446
            }
447
        }
448
 
449
        @Override
450
        public void flush() throws IOException {
451
            this.innerStream.flush();
452
        }
453
 
454
        @Override
455
        public void write(byte[] buffer, int offset, int count) throws IOException {
456
            this.innerStream.write(buffer, offset, count);
457
        }
458
 
459
        @Override
460
        public void write(byte[] buffer) throws IOException {
461
            this.innerStream.write(buffer);
462
        }
463
 
464
        @Override
465
        public void write(int oneByte) throws IOException {
466
            this.innerStream.write(oneByte);
467
        }
468
    }
469
 
470
    private static final class CopyingInputStream extends InputStream {
471
        final InputStream input;
472
        final OutputStream output;
473
 
474
        CopyingInputStream(final InputStream input, final OutputStream output) {
475
            this.input = input;
476
            this.output = output;
477
        }
478
 
479
        @Override
480
        public int available() throws IOException {
481
            return input.available();
482
        }
483
 
484
        @Override
485
        public void close() throws IOException {
486
            // According to http://www.cs.cornell.edu/andru/javaspec/11.doc.html:
487
            //  "If a finally clause is executed because of abrupt completion of a try block and the finally clause
488
            //   itself completes abruptly, then the reason for the abrupt completion of the try block is discarded
489
            //   and the new reason for abrupt completion is propagated from there."
490
            //
491
            // Android does appear to behave like this.
492
            try {
493
                this.input.close();
494
            } finally {
495
                this.output.close();
496
            }
497
        }
498
 
499
        @Override
500
        public void mark(int readlimit) {
501
            throw new UnsupportedOperationException();
502
        }
503
 
504
        @Override
505
        public boolean markSupported() {
506
            return false;
507
        }
508
 
509
        @Override
510
        public int read(byte[] buffer) throws IOException {
511
            int count = input.read(buffer);
512
            if (count > 0) {
513
                output.write(buffer, 0, count);
514
            }
515
            return count;
516
        }
517
 
518
        @Override
519
        public int read() throws IOException {
520
            int b = input.read();
521
            if (b >= 0) {
522
                output.write(b);
523
            }
524
            return b;
525
        }
526
 
527
        @Override
528
        public int read(byte[] buffer, int offset, int length) throws IOException {
529
            int count = input.read(buffer, offset, length);
530
            if (count > 0) {
531
                output.write(buffer, offset, count);
532
            }
533
            return count;
534
        }
535
 
536
        @Override
537
        public synchronized void reset() {
538
            throw new UnsupportedOperationException();
539
        }
540
 
541
        @Override
542
        public long skip(long byteCount) throws IOException {
543
            byte[] buffer = new byte[1024];
544
            long total = 0;
545
            while (total < byteCount) {
546
                int count = read(buffer, 0, (int)Math.min(byteCount - total, buffer.length));
547
                if (count < 0) {
548
                    return total;
549
                }
550
                total += count;
551
            }
552
            return total;
553
        }
554
    }
555
 
556
    public static final class Limits {
557
        private int byteCount;
558
        private int fileCount;
559
 
560
        public Limits() {
561
            // A Samsung Galaxy Nexus can create 1k files in half a second.  By the time
562
            // it gets to 5k files it takes 5 seconds.  10k files took 15 seconds.  This
563
            // continues to slow down as files are added.  This assumes all files are in
564
            // a single directory.
565
            //
566
            // Following a git-like strategy where we partition MD5-named files based on
567
            // the first 2 characters is slower across the board.
568
            this.fileCount = 1024;
569
            this.byteCount = 1024 * 1024;
570
        }
571
 
572
        int getByteCount() {
573
            return byteCount;
574
        }
575
 
576
        int getFileCount() {
577
            return fileCount;
578
        }
579
 
580
        void setByteCount(int n) {
581
            if (n < 0) {
582
                throw new InvalidParameterException("Cache byte-count limit must be >= 0");
583
            }
584
            byteCount = n;
585
        }
586
 
587
        void setFileCount(int n) {
588
            if (n < 0) {
589
                throw new InvalidParameterException("Cache file count limit must be >= 0");
590
            }
591
            fileCount = n;
592
        }
593
    }
594
 
595
    // Caches the result of lastModified during sort/heap operations
596
    private final static class ModifiedFile implements Comparable<ModifiedFile> {
597
        private static final int HASH_SEED = 29; // Some random prime number
598
        private static final int HASH_MULTIPLIER = 37; // Some random prime number
599
 
600
        private final File file;
601
        private final long modified;
602
 
603
        ModifiedFile(File file) {
604
            this.file = file;
605
            this.modified = file.lastModified();
606
        }
607
 
608
        File getFile() {
609
            return file;
610
        }
611
 
612
        long getModified() {
613
            return modified;
614
        }
615
 
616
        @Override
617
        public int compareTo(ModifiedFile another) {
618
            if (getModified() < another.getModified()) {
619
                return -1;
620
            } else if (getModified() > another.getModified()) {
621
                return 1;
622
            } else {
623
                return getFile().compareTo(another.getFile());
624
            }
625
        }
626
 
627
        @Override
628
        public boolean equals(Object another) {
629
            return
630
                    (another instanceof ModifiedFile) &&
631
                    (compareTo((ModifiedFile)another) == 0);
632
        }
633
 
634
        @Override
635
        public int hashCode() {
636
            int result = HASH_SEED;
637
 
638
            result = (result * HASH_MULTIPLIER) + file.hashCode();
639
            result = (result * HASH_MULTIPLIER) + (int) (modified % Integer.MAX_VALUE);
640
 
641
            return result;
642
        }
643
    }
644
 
645
    private interface StreamCloseCallback {
646
        void onClose();
647
    }
648
}